Compare commits

...
174 Commits
Author SHA1 Message Date
Peer Richelsen de446db434 v2.4.3
release.yml / release (push) Canceled after 0s
2023-01-04 21:00:14 +01:00
81546412bf add missing variables for variable translations (#6271)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-04 19:57:47 +00:00
Peer Richelsen 459530fba3 Revert "sidebar improvements" (#6273) 2023-01-04 15:46:42 +00:00
Marlo Kesser 42766b42c0 fix localization issues with default locations (#5569) 2023-01-04 15:31:50 +00:00
Hariom Balhara 38b3563350 Feature/Routing Forms: Add the concepts of reusable Router and Router Linked Fields. (#5566)
* WIP

* Fix all TS errors

* Move to using isRouter everywhere

* wip

* Add Router Picker and fix TS issues

* Fix tests and add option to connect during duplication

* Misc changes

* Fixes

* Fixes

* Fix TS errors

* Disable certain options in Router

* Fix connected Form fields not updating

* Some fixes

* reert shell.tsx

* Disable already used router instead of removing it

* Import prisma from correct place

* Simplify loop using Object.entries

* SimpliAdd playwright test and fix issues found
2023-01-04 13:30:46 +00:00
Jeroen Reumkens 37159914cf 🧹 Removed old root UI components. (#6249)
* Removed old root UI components.

* Fixed import path

* Changed icon to not import from root, since it somehow creates a recursive import

* Removed irrelevant todo

* Fixed merge issues.

* Fixed shell export
2023-01-04 14:05:00 +01:00
Peer Richelsen 268bf68532 sidebar improvements (#6259)
* added bottom-0 to sidebar nav

* open sidebar on right for RTL
2023-01-04 11:15:30 +00:00
sean-brydon b02c7e01d5 Move Apps to ui/components (#6245)
* Move Apps to components

* Update packages/features/ee/sso/page/teams-sso-view.tsx
2023-01-04 11:00:01 +00:00
45d10a3bc4 a11y: right-to-left improvements (#6235)
* fixed a ton of rtl issues

* removed duplicant

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-04 07:38:45 +00:00
Jeff LoiselleandPeer Richelsen 9250b20c1d fixes available times overflow bug on safari (#6208)
* fixes available times overflow bug on safari

* WIP

* remove extranous grid classes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-03 22:05:40 +00:00
sean-brydon d9da311141 Move Skeleton to ui/components (#6247)
* Inital Move

* Skeleton story
2023-01-03 15:54:00 +00:00
Nafees Nazik 89a52dad21 fix: cutoff text and align text (#6248) 2023-01-03 14:12:36 +00:00
Hariom Balhara 2a1acc1294 Fix lang and dir attributes not being set (#6243) 2023-01-03 11:09:05 +01:00
8302990f84 fix: use hasMounted to render calcom versionm (#6239)
* fix: use hasMounted to render calcom versionm

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

* added Credits component but struggling with import

* only made it work with v2 folder. sorry jerone

* added link to releases

* added link to cal.com

* removing script calcomversion for now until we need it

Signed-off-by: Udit Takkar <udit.07814802719@cse.mait.ac.in>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2023-01-02 19:44:51 +00:00
LoneRifleandPeer Richelsen 5a03b61d63 refactor(web): remove fonts.css, rely on shared-globals.css (#6220)
`styles/fonts.css` is identical to `shared-globals.css`, as found in
`packages/ui/styles`, and as referenced by `styles/globals.css`. Remove
`fonts.css` and its references, leaning instead on the fonts described
in `shared-globals.css`

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-02 19:11:06 +00:00
github-actions[bot]andCrowdin Bot 6b6f239936 New Crowdin translations by Github Action (#6242)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-02 19:50:31 +01:00
Jeff LoiselleandPeer Richelsen 9c2748e8e9 adds team prefix to url when creating team (#6211)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-02 18:34:32 +00:00
Peer Richelsen bb5cc33737 Update README.md 2023-01-02 18:05:53 +01:00
Udit TakkarandPeer Richelsen 5616a64827 fix: use border-0 (#6233)
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-01-02 15:10:00 +00:00
33530ad8c8 Make cal.com version available on all pages, even on login page. (#6221)
* Make cal.com version available on all pages, even on login page.

* Extract out calcom version as a utility

* added version to login screen

* added ee and ce to version

* fixed lint

* Fix import

* removed  version info component

Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-02 20:24:35 +05:30
Peer Richelsen fe14839f1b fixed sidebar position, added toHide to <LicenseRequired> (#6228)
* fixed sidebar position

* fixed sidebar position

* fixed sidebar position

* removed gradient overlay from user dropdown

* added toHide prop to license check to hide a component if a license is available

* show children if toHide is true
2023-01-02 10:13:27 +00:00
Hariom Balharaandkodiakhq[bot] 15123c1aa5 Add disable success page config[Platform Only] (#6225)
* Add disable success page config

* Change text

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2023-01-02 08:41:39 +00:00
Peer Richelsen 71ee9c72fe introduced size="fab" prop to Button, added to CTA buttons (#6176)
* introduced size="fab" prop to Button, added to CTA buttons

* increased size of FAB

* nit
2023-01-02 09:28:12 +01:00
github-actions[bot]andCrowdin Bot 5503539856 New Crowdin translations by Github Action (#6222)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2023-01-01 13:32:19 +01:00
sean-brydon 6548f01efa Show admin password top banner (#6226)
* Invalid-admin fake role

* Reset password link + shell
2023-01-01 11:19:58 +00:00
Udit TakkarandPeer Richelsen 518a4484d6 fix: add allowScroll prop (#6215)
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>
2022-12-30 18:16:37 +00:00
Siddhant KhareandPeer Richelsen 3535a25a59 fix: consistency on menu hover border bug 🔧 (#6218)
Signed-off-by: Siddhant Khare <Siddhantkhare2694@gmail.com>

Signed-off-by: Siddhant Khare <Siddhantkhare2694@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-30 18:04:54 +00:00
Stefano Maffeis 06cc6741e6 Make radio field id unique by adding custom input id to their identifier (#6224) 2022-12-30 18:52:03 +01:00
Peer Richelsen ed1478e1ba Update README.md 2022-12-30 10:17:31 +01:00
Jeff Loiselle 854c05d673 fixes required custom radio element on firefox (#6209)
Fixes https://github.com/calcom/cal.com/issues/6160
2022-12-28 19:43:18 +00:00
bcf5fb18c5 Cal Video (Daily) Recording (#6039)
* feat: wip

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

* feat: add download recording button

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

* moved video recording into /ee/, wrapped in LicenseRequired

* fix: security issues in downloading recording
updates designs

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

* feat: add upgradation banner

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

* chore: remove default room

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

* chore: fix type error

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

* fix: add type in get recording

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

* fix: add suggestions and zod type for recording

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

* fix: add types in getRecordings

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: type error

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

* fix: finally all type errors fixed

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

* Server side validation for users in team plans for recordings

* fix: remove any type

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>
Co-authored-by: Peer Richelsen <peer@cal.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Alan <alannnc@gmail.com>
2022-12-27 21:03:39 +00:00
3f822e814b event type setting refactor (#5959)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-26 10:55:58 +00:00
Peer Richelsen 4fd25c7215 fixed booking layout (#6188) 2022-12-26 16:11:28 +05:30
github-actions[bot]andCrowdin Bot f42bed2a55 New Crowdin translations by Github Action (#6175)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-26 11:25:47 +01:00
469c205033 fix: setting sidebar improvements (#6177)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Peer Richelsen <peer@cal.com>
2022-12-26 11:23:03 +01:00
Nafees Nazik 31551f3b9e fix: button focus (#6178) 2022-12-26 09:44:43 +00:00
Carina WollendorferandCarinaWolli 69925af17c Fix skeleton loader for /workflows (#6172)
* return latest bookings for example bookings for zapier

* remove mt on skeleton loader

* Revert "return latest bookings for example bookings for zapier"

This reverts commit b69f393a73a59a05fe99f01d8caa4ac0fd5cf992.

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-23 12:27:40 +00:00
Carina WollendorferandCarinaWolli 2502868ebc Return latest bookings for zapier example bookings (#6170)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-23 12:00:03 +01:00
zomars 38d242add9 v2.4.2
release.yml / release (push) Canceled after 0s
2022-12-22 17:12:32 -07:00
zomars c272e281cf Submodule sync 2022-12-22 17:12:13 -07:00
zomars d4e19b0fa4 Revert "Matches postgres image across projects"
This reverts commit c12877a58f.
2022-12-22 17:11:31 -07:00
zomars c12877a58f Matches postgres image across projects 2022-12-22 16:52:44 -07:00
af83350d04 replaced webhook icon with actual webhook logo (#6161)
* replaced webhook icon with actual webhook logo

* Apply suggestions from code review

* Linting

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-22 12:57:44 -07:00
sean-brydonandPeer Richelsen 9d0cf6be0b No more modules folder 🔥 (#6158)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-22 19:06:26 +00:00
zomars 2fe9c782d4 Lowercase usename checks again. 2022-12-22 11:09:31 -07:00
zomars d0a3d8d144 Link fixes
commit 56c1608deaf1921a80dc71bcbd107e772dd656f4
Author: Omar López <zomars@me.com>
Date:   Wed Dec 21 19:26:04 2022 -0700

    Update link.ts

commit eab43d833515cabc7acf2f973c2d2837ac1a0332
Author: zomars <zomars@me.com>
Date:   Wed Dec 21 19:03:02 2022 -0700

    Prevents 500 on TRPC error

commit 3a6970d5e5fab54136b379e1b2ccd7acf515a809
Author: zomars <zomars@me.com>
Date:   Wed Dec 21 18:15:26 2022 -0700

    Encoding fixes
2022-12-22 10:52:50 -07:00
sean-brydonandzomars cefcb1eaf8 Feature/booking filters (#6099)
* EventTypeFilter UI working

* Tidy + fix avatar + fix overflow

* Component store

* WIP - switchin to query filtering instead of state

* Push working

* Checkbox approach - weird conditional toggle for all bookings

* WIP

* the master query hook :O

* useTypedQuery

* Working typed query

* Typed query hook

* Add optional keys

* safeParse query params

* Add use callback

* Fix re-renders

* Remove local version of local query and use util branch

* Working team filters!

* working people filter!

* Tidy up

* NIT

Co-authored-by: zomars <zomars@me.com>
2022-12-22 12:35:01 +00:00
sean-brydonandPeer Richelsen 83b60de640 Chor/move table actions (#6157)
* Move table actions

* Move table actions

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-22 12:41:43 +01:00
Hariom BalharaandAlex van Andel a076418989 Hotfix/Event Type Save Error with Multiple Durations (#6154)
* Fix .length being undefined when form is submitted without any change to duration

* Fix type error

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-22 11:13:33 +00:00
zomars 64ecce1ccb Submodule sync 2022-12-21 17:49:10 -07:00
zomars cd56a5d4f0 Removes deprecated rejectOnNotFound 2022-12-21 17:48:15 -07:00
zomars d5ca121ac6 Revert "team banner (#5956)"
This reverts commit 463787b607.
2022-12-21 17:39:07 -07:00
zomars 1e20e80b65 v2.4.1
release.yml / release (push) Canceled after 0s
2022-12-21 17:18:38 -07:00
github-actions[bot]andCrowdin Bot 75ac3fd40c New Crowdin translations by Github Action (#6150)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-21 17:17:43 -07:00
Alex van Andelandkodiakhq[bot] 3b91cc7f02 75px -> 88px (#6152)
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-21 17:17:24 -07:00
23450b61e0 Refactor/email confirm links (#6151)
* Booking confirmation link improvements

* Cleanup

* Update webhooks payload

* Removing unneeded dependency

* Feedback

* Matches enum style to rest of codebase

Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Leo Giovanetti <hello@leog.me>
2022-12-21 17:15:51 -07:00
zomars dcc226976c Update labeler.yml 2022-12-21 17:03:00 -07:00
zomars 196fde72f6 Update labeler.yml 2022-12-21 16:59:19 -07:00
github-actions[bot]andCrowdin Bot 1e913cd406 New Crowdin translations by Github Action (#6149)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-21 22:16:32 +01:00
02425702d1 Fixes/stripe improvements (#5698)
* Enables require confirmation after payment its made

* Various Stripe improvements

* Use constant

* Merge branch 'main' into fixes/stripe-improvements

* Update getStripeAppKeys.ts

* submodule sync

* Submodule sync

* Revert

* Update apps/web/components/booking/BookingListItem.tsx

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

Co-authored-by: Alan <alannnc@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-21 20:31:09 +00:00
github-actions[bot]andCrowdin Bot 2494655369 New Crowdin translations by Github Action (#6148)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-21 21:18:49 +01:00
0e0a78c47e Fix/Round Robin (#6121)
* Identify owner for a timeslot and check busyness against his schedule only

* Fix TS errors

* Fix flatmap

* Fix xisting unit tests

* Prevent duplicate slots by merging, userId -> userIds (multi)

* Small fix to potential undefined

* Moved duplicate prevention to buildSlots function

* Apply date override on a per user basis

* Prevent -1 being added to computedLocalAvailability

* Removed console.log

* Apply override properly on COLLECTIVE

* Default timeZone to UTC when undefined

* isSame doesn't work when the day shifts, isBetween instead

* Iterate over all slot.userIds schedules to identify the first user schedule that matches

* add round-robin test

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-21 19:32:42 +00:00
Leo GiovanettiandAlex van Andel ed02c10f54 Fixing event type duration query param (#6146)
* Fixing event type duration query param

* Ignoring query param instead of erroring out

* Removing unneeded string

* Amended useEffect for simplication

* Refactored useRouterQuery into new useTypedQuery

* Revert "Refactored useRouterQuery into new useTypedQuery"

This reverts commit b3b0aa6077a75ae37c0220cce5044b7655ff602f.

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-21 17:23:02 +00:00
github-actions[bot]andCrowdin Bot 9c40f76184 New Crowdin translations by Github Action (#6140)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-21 09:45:47 -07:00
Alex van AndelandJoe Au-Yeung 8eda30d88c Add 23:59 as a valid availability select option (#6137)
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2022-12-21 16:13:50 +00:00
alannncandAlex van Andel 9de19d35a8 Change startTime to current date - 2 days if startTime it's on current month (#6117)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-21 15:11:42 +00:00
0770403ca5 Improved borders on teams profile pages. (#6141)
* Improved borders on teams profile pages.

* Improved border styles on dynamic booking pages (pro+free) and improved darkmode avatar border.

* Fixed badge hover state in dark mode

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-21 15:06:00 +01:00
Mohammed CherfaouiandAlex van Andel 1a45cb35c1 remove cancel button for unconfirmed past bookings (#6142)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-21 13:43:31 +00:00
Jeroen ReumkensandPeer Richelsen 15cdff76d9 Feat/add screenshot og image variant (#6138)
* Added screenshot variant of og image (used in website).

* Added option to rotate background image in og image.

* Converted wrapper props to interface instead of inline type declaration.

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-21 10:39:14 +00:00
Hariom Balhara 8c4eb4b5fe Fix/Embed - Fix for Duration query param getting removed (#6139) 2022-12-21 15:44:56 +05:30
zomars ae30aafafe Fixes scroll on medium screens 2022-12-20 18:27:16 -07:00
3b74717591 Fix: user impersonation switch showing the wrong value (#6126)
* fix: remove register method because ref is not forwarded

* fix: prefetch user to get the correct value for the switch component

* feat: add optimistic update

* feat: disable update button if not dirty

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2022-12-20 23:13:31 +00:00
Leo Giovanetti 7a6349b4c0 [Hotfix] Fixing direct link encoding (#6131) 2022-12-20 16:00:33 -07:00
3b51fff88d New Crowdin translations by Github Action (#6134)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-20 23:48:06 +01:00
+5 2a0b0be9d0 [CAL-211] Confirmed booking page for rescheduled event should show previous time and reschedule reason (#5369)
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: José Romary Brandão Jr <joseromarybrandao@gmail.com>
Co-authored-by: csangam <coolmagnas@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>

Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Matheus Benini <matheus_benini@hotmail.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: José Romary Brandão Jr <joseromarybrandao@gmail.com>
Co-authored-by: csangam <coolmagnas@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-20 22:46:28 +00:00
f373c9d818 Webhooks for team event-types (#6120)
* add webhooks tab for team event types

* show nr of active event types

* type fixes + show team webhooks only in event type single view

* adding and deleting webhook

* fix disabling and enabling team webhooks

* add editing webhook

* code clean up

* add installed apps

* code clean up

* fix reserved subscriber url when creating event

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-20 15:34:37 -07:00
540bf3a1cb Fix app categories (#6016)
* Fix app metas

* Optimize categories on app store

* Remove commented code

* Remove console log

* Update apps/web/pages/apps/index.tsx

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

* Add categories to missing app metadata

* Fix type error

* Type fix

* Type fixes

* More type fixes

* Clean up

* Fix build error

* No leaky please

* Remove comment

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-20 15:15:06 -07:00
2d9064e92f fix: show email in outlook (#5897)
* fix: change external id to owner address

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

* feat: add email prop in calendar

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

* fix: only display for outlook

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

* fix: add email for apple calendar

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

* fix: type error and use description

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

* fix: try  using email from /me endpoint

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

* fix: fallback to principal name when mail is not there

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

* chore: use empty string for fallback

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: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2022-12-20 21:50:20 +00:00
alannncandPeer Richelsen 9487819ec1 Add try catch for event type duplicate, fixed catch error loggin (#6130)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-20 21:38:15 +00:00
zomars 49bf14b196 Submodule sync 2022-12-20 13:42:01 -07:00
344f16d6cc Fixing multiple duration default issues (#6114)
* Fixing multiple duration default issues

* Ensure that when calculation time slots we use 1 as a minimum interval, since 0 min meetings would result in an infinite loop.

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-20 17:49:24 +00:00
github-actions[bot]andCrowdin Bot ccbf3b1e54 New Crowdin translations by Github Action (#6125)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-20 16:22:25 +01:00
Nafees Nazik dcf0fad35d fix: remove duplicate meta component (#6124) 2022-12-20 15:13:30 +00:00
github-actions[bot] a836800c69 New Crowdin translations by Github Action (#6123) 2022-12-20 13:49:10 +00:00
github-actions[bot]andCrowdin Bot b8c6ac43f7 New Crowdin translations by Github Action (#6122)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-20 14:42:23 +01:00
f5e0c6b384 feat: disable update button unless any input value changed in general settings (#5686)
* feat: disable update button unless any input value changed

* fix: reset dirtyFields on success

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
2022-12-20 13:25:15 +00:00
sean-brydonandAlex van Andel 2a5dd9035f Fixing booking limits (#6119)
* Fix logic

* Some performance improvements

* Return early to reduce depth

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-20 13:10:54 +00:00
2cddad4336 Typed query hook (#6034)
* Typed query hook

* Add optional keys

* safeParse query params

* Add use callback

* Fix re-renders

* Add schemas to support query params - fix keys

* Update packages/lib/hooks/useTypedQuery.ts

* Fix submodules

* Fix website submodule

* Reset website submodule

* Merry Xmas Sean

* Whoops

Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-19 22:34:46 +00:00
Peer Richelsen 4bcbff51b1 /event-types improvements (#6113) 2022-12-19 19:35:01 +00:00
Peer Richelsen ae541e4c37 /bookings: only show timezone popover on hover (#6112) 2022-12-19 13:56:34 -05:00
Omar López f254711d85 Makes WizardForm translation agnostic (#6111) 2022-12-19 11:30:54 -07:00
Nafees Nazik 533e208643 Fix: Position of the "Add" button (#6110)
Fixes https://github.com/calcom/cal.com/issues/6104
2022-12-19 12:53:46 -05:00
Jeroen ReumkensandPeer Richelsen 795574ef88 Ensure topbanner is always on top of page layout rather than inside main content (#6098)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-19 17:37:20 +00:00
Nafees NazikandPeer Richelsen 48611d5c99 Fix: pending payment label appears twice (#6109)
* fix: remove duplicate label

* feat: replace Tag component with the badge component

* refactor: remove unused Tag component

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-19 16:59:11 +00:00
Leo GiovanettiandPeer Richelsen 11a07961d0 Fixing permission for admin wizard (#6061)
* Fixing permission

* Update apps/web/pages/auth/setup/index.tsx

* Correcting error page code

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-19 14:50:25 +00:00
6be0fd52e7 fix: add a returnTo query param in new team url path (#5967)
* fix: remove optional from schema.

* schema success state is always true  because of the optional property.
 so we are getting undefined if returnTo query Param doesn't exist

* fix: add a return to query param

* nit: turn querySchema into an object

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-19 15:38:33 +01:00
Joe Au-Yeung 78da7ba338 Set seatsShowAttendees to false (#6025) 2022-12-19 14:12:18 +00:00
9a72c648c5 Removes EMAIL_ADDRESS action (#6102)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-19 13:50:14 +00:00
Leo Giovanetti 3e42da7fc7 Fixing wizard validation (#6068) 2022-12-19 13:34:40 +00:00
github-actions[bot]andCrowdin Bot 1d986cad04 New Crowdin translations by Github Action (#6101)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-19 12:41:52 +01:00
Nafees Nazik f603e4eff2 fix: add contact_our_support_team i18n key (#6097) 2022-12-19 12:37:41 +01:00
sean-brydonandPeer Richelsen 1f4e6c1987 Migrates V2 Select to new ui/components (#5400)
* v2 select to components folder + docs

* Fix type errors

* fix type checks

* Fix imports

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-18 23:16:20 +00:00
Hariom Balhara e57f734e79 Test/Embed/Reschedule (#6056)
* Add reschedule embed test

* Add comments
2022-12-18 19:24:44 +00:00
github-actions[bot]andCrowdin Bot 7fad76ae23 New Crowdin translations by Github Action (#6095)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-18 13:52:44 +01:00
Maxim GandAlex van Andel 7659a016b4 Use the same locale for event description. (#5674)
* Use the same locale for event description.

Currently all headers  of the event description use organizer's language except description part. This is not suitable to see different languages withing the same text block.

Change description header to use the same locale as others.

* Translate app_status in organizer language too

Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-18 12:36:30 +00:00
Leo GiovanettiandPeer Richelsen 34fe1c4a95 Fixing navigation and visual issues (#6067)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-18 10:56:21 +00:00
Mehran ShahbazandPeer Richelsen 205f73c4bc React feather refactor (#6090)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-18 10:44:44 +00:00
Peer Richelsen c4bd2167e1 pluralise german strings (#6092) 2022-12-18 10:50:51 +01:00
github-actions[bot]andCrowdin Bot 49a7111b66 New Crowdin translations by Github Action (#6089)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-18 10:43:31 +01:00
Carina WollendorferandCarinaWolli ca045a040b add meeting url to dynamic text variables of custom workflow templates (#5958)
* small design fix for time unit select

* save video cal url to booking in metaData

* save video url when bookings are confirmed

* fix e2e tests

* remove not needed waitForNavigation

* add metadata to booking created webhook payload

* add migration

* fix type error

* add meeting url variable to dropdown

* add meeting url to custom template

* add BookingMetaDataSchema

* add meeting url to email reminder

* add meeting url to sms notifcation

* adjust e2e tests

* add bookingMetadataSchema

* add missing change from merge

* fix spelling

* fix type error

* code clean up

* fix e2e snapshot

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-18 02:04:06 +00:00
fiskareandAlex van Andel a8b4198bf7 Update common.json (#6080)
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-17 23:10:40 +00:00
Alex van Andel 2ab24e530b Nope on test (#6085)
* Nope on test

* Use // instead of /**/

* Investigating why types are failing
2022-12-17 22:51:00 +00:00
734b06dfdf fix: prevent overflow in booking description (#6077)
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: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-17 22:49:15 +01:00
github-actions[bot] 467084993b New Crowdin translations by Github Action (#6082) 2022-12-17 18:42:35 +00:00
Leo Giovanetti 1e2c8516d2 Workflow direct email disabling (#6075)
* Disabling direct emails from workflow (#6074)

* Commenting out the option fro logic too
2022-12-17 11:11:59 -07:00
Leo Giovanetti d57738f325 Disabling direct emails from workflow (#6074) 2022-12-17 12:26:54 +01:00
Carina WollendorferandCarinaWolli c39a7dba85 don't send scheduled (#6072)
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-16 22:58:43 +00:00
Carina WollendorferandCarinaWolli fd37397e5c Fix/workflow email (#6069)
* teams plan needed

* disable

* disable

* type fix

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-16 21:11:08 +00:00
df0a522656 fix: use callback url in signup (#6010)
* fix: use callback url in signup

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

* fix: do login after sign up and redirect to onboarding

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

* chore: add telemetry

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

* fix: error handling

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: Omar López <zomars@me.com>
2022-12-16 20:40:20 +00:00
916468b8cc feat: adding custom field of type PHONE (#5623)
* feat: adding custom field of type PHONE

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

* fix: null phone number bug and removed console log

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

* fix: decrease iteration

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: Omar López <zomars@me.com>
2022-12-16 12:39:41 -07:00
Peer Richelsen b789e58624 added emptyscreen for app store search (#6065)
* added emptyscreen for app store search

* nit
2022-12-16 16:02:29 -03:00
Leo Giovanetti 4368536141 Fixing Admin Wizard step 1 blocking step 2 (#6063) 2022-12-16 18:41:07 +01:00
Alex van Andel 9eef9766ce Truncate month from November to Nov on Booking page (#6058) 2022-12-16 13:35:56 +00:00
Peer Richelsen 68dbfbd7d0 amie.so app (#6053)
* added amie

* changed category

* fixed category
2022-12-16 13:11:32 +00:00
Alex van Andel 692a84a86e Fixes the start and end time mobile view (#6057) 2022-12-16 12:46:15 +00:00
Alex van AndelandPeer Richelsen 1671095c94 Fixes to dialog for date override & dialog overflow fix (#6055)
* Fixes to dialog for date override & dialog overflow fix

* Fixed datepicker border on mobile

* removed max-height from dialog, needs testing

Co-authored-by: Peer Richelsen <peer@cal.com>
2022-12-16 13:27:42 +01:00
zomars 3b896bee5c v2.4.0
release.yml / release (push) Canceled after 0s
2022-12-15 15:59:08 -07:00
zomars cc1985442e Submodule sync 2022-12-15 15:58:13 -07:00
Omar López 630f438b32 Upgrades prisma / Syncs packages (#6044)
* Syncs packages

* Upgrades zod
2022-12-15 15:56:48 -07:00
6fae01d45a added auto animate to installed apps, fixed container size (#6046)
* added auto animate to installed apps, fixed container size, horizontal scroll

* Update packages/ui/v2/modules/booker/DatePicker.tsx

Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-15 22:47:31 +00:00
dependabot[bot] 7009373b11 Bump loader-utils from 1.4.0 to 1.4.2 (#6045)
Bumps [loader-utils](https://github.com/webpack/loader-utils) from 1.4.0 to 1.4.2.
- [Release notes](https://github.com/webpack/loader-utils/releases)
- [Changelog](https://github.com/webpack/loader-utils/blob/v1.4.2/CHANGELOG.md)
- [Commits](https://github.com/webpack/loader-utils/compare/v1.4.0...v1.4.2)

---
updated-dependencies:
- dependency-name: loader-utils
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2022-12-15 15:18:40 -07:00
c2f150dbab Feat/verify phone number (#6035)
* first implementation of verifying phone number

* add UI + logic for verifying phone number flow

* check if all phone numbers are verified before submit

* add numberVerification pending

* only send SMS to verified numbers

* fix design for mobile view

* check if phone number is verified before testing action

* add back message service id check

* add TWILIO_VERIFY_SID to .env.example

* code clean up

* add TWILIO_VERIFY_SID to README.md instructions

* save new verified numbers

* fix wrongly thrown error for new verified numbers

* use false as default value for isVerificationPending paramater

* add translations

* add migration file

* code clean up

* don't throw error if phone number is not needed

* Feedback

* Shows error when Twillio isn't properly setup

* Type fixes

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-15 21:54:40 +00:00
688541923b Save meeting url to metadata of bookings and add it to webhook payload (#5773)
* small design fix for time unit select

* save video cal url to booking in metaData

* save video url when bookings are confirmed

* fix e2e tests

* remove not needed waitForNavigation

* add metadata to booking created webhook payload

* add migration

* fix type error

* adjust e2e tests

* add bookingMetadataSchema

* add missing change from merge

* add new line to expected test result

* update snapshot

* fix e2e tests

* Update confirm.ts

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2022-12-15 21:43:07 +00:00
20402ac26c Fixes for testing workflow action (#6005)
* additional checks for testing workflow action

* fix old variable in testAction mutaiton

* remove workflowId

* only allow team plan to test action

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
2022-12-15 20:48:22 +00:00
Omar López a54952b41a Adds tests for date overrides (#6027) 2022-12-15 13:19:35 -07:00
github-actions[bot]andCrowdin Bot 987756c4d4 New Crowdin translations by Github Action (#6022)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-15 21:18:33 +01:00
Leo Giovanetti 3ee294ffc5 Fixing admin wizard translations (#6038) 2022-12-15 15:03:57 +00:00
Leo Giovanetti a638708871 Baikal CalDAV error hint (#6021) 2022-12-15 13:45:54 +00:00
Peer RichelsenandHariom Balhara 3456cc5521 vimcal app (#5791)
* wip vimcal app

* added vimcal blog link

* todo: filter out external calendar apps

* Make it a non calendar type app with category and variant as calendar

Co-authored-by: Hariom Balhara <hariombalhara@gmail.com>
2022-12-15 08:44:21 +00:00
GitStart 546f0c3881 A Playwright test for "Copy Typeform Redirect URL link" action (#5716)
Co-authored-by: niteshsingh1357 <niteshsingh1357@gmail.com>
Co-authored-by: Nitesh Singh <nitesh.singh@gitstart.dev>
Co-authored-by: gitstart <gitstart@users.noreply.github.com>
Co-authored-by: RubensRafael <rubensrafael2@live.com>
Co-authored-by: José Romary Brandão Jr <joseromarybrandao@gmail.com>
Co-authored-by: csangam <coolmagnas@gmail.com>
Co-authored-by: Rafael Toledo <87545086+Toledodev@users.noreply.github.com>
Co-authored-by: gitstart <gitstart@gitstart.com>
Co-authored-by: Rubens Rafael <70234898+RubensRafael@users.noreply.github.com>
Co-authored-by: Matheus Benini Ferreira <88898100+MatheusBeniniF@users.noreply.github.com>
Co-authored-by: gitstart-app[bot] <57568882+gitstart-app[bot]@users.noreply.github.com>
Co-authored-by: Thiago Nascimbeni <tnascimbeni@gmail.com>
Co-authored-by: Matheus Muniz <87545749+matheusmuniz03@users.noreply.github.com>
Fixes https://github.com/calcom/cal.com/issues/4570
2022-12-15 11:15:09 +05:30
7b18272d60 SMS workflow actions only for Teams (#6003)
* disable sms actions for users without team

* add isTeamsPlan query

* disable SMS action if user is not in a team

* remove console log

* disable SMS actions also in AddActionDialog

* throw error is users wants to add sms or edit  to sms action

* code clean up

* Get actions options from backend. cleanup

* Typefix

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-15 00:11:57 +00:00
Peer Richelsen cf6efabdbb added layout for empty month to go to the next month (#6011) 2022-12-14 22:43:21 +00:00
Udit TakkarandPeer Richelsen 92d15a00f5 fix: use cal url (#6013)
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>
2022-12-14 17:43:00 +00:00
2f2b72dd54 Feature/date overrides (#5991)
* Initial incomplete (but mostly functional) push of date override functions

* Fixed date shifting on load

* Bring back minDate (automatically disable all dates before current date)

* Fix type error

* Supply working hours to render available dates

* Converted to SSR

* moving defaultValues to the backend

* Improv. as filter can be achieved within the reduce

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

* Double inversion -> single, as it is an early return

* uniq() exit - not needed anymore

* Typefixes

* It's overriding dates :D

* Fixed duplication DateOverrides in list

* Implemented changing the month

* Make dateOverrides an optional param

* Fixed test (which requires dateOverrides due to auto-typing)

* Prevent a full update on set as default from list view

* Added some extra keys to keep ts happy

* Only allow a single date override per date

* Disallow editing excludedDates to the same date

* Bring back duplicate key ?.?

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-14 17:30:55 +00:00
8fd5d6b5b5 Calendar Weekly Scheduler (#5653)
* storybook v2 init

* Merge config into storybook vite build

* Remove path

* Storybook config tweaks

* Added styles and settings for storybook v2, and started working on button documentation and examples.

* Badges + flex wrap on mobile

* Breadcrumbs+button+avatar

* Checkbox

* Input + moving files around

* WIP table

* WIP table grid

* Replaced imports for new components.

* Added first steps for varianttable.

* Small alignment fix.

* Custom Args Table - With scrollbar

* Adding table to components that need it + darkmode

* Add intro

* Fix types

* Remove V1 storybook and replace with V2

* Fix badge type error

* Fixed storybook dependencies

* Added cover image to storybook

* Remove vita from ts config, we dont use vite.

* Fixed button import.

* Explained postcss pseudo plugin.

* Fixed badge import.

* Add Avatar Stories

* ButtonGroup Stories

* Fixed imports

* Add checkbox stories

* Inital state plannning

* Inital state combined with passed in props

* Start of UI work

* Able to change dates?

* Add dynamic hour props

* Get grid system setup correctly

* Show events on grid

* Weird sizing issue but events placed correctly gridstart

* CAL styled calendar event component

* availability WIP ish

* Blocking days! + Block days < today

* Kinda working time line

* Rename grid stop + formatting

* Handle sorting events if required.

* Add util for getting startDate bassed on weekday

* Remove event stories for now

* Implement gridstops per hour to be dyamic

* New CSS Grid + offsetbased positoning

* Fix weird Z-Index issues on hover

* Implement blocklist again with new format

* Side by side events working - styling needs work

* New design of overlap

* Overlapping? Working :O

* Cleanup

* WIP hover state

* Werid border issue

* fix translate issue

* Kinda working with overflow

* Fix overflow

* Progressive date blocking

* Cleanup

* Fix double render of blocked list

* WIP mobile implementaiton

* Trying to fix CSS

* Extract CSS to styles.css to allow media queries

* Improve documentation - allow args to be changed in storybook

* Fix hover showing even if disabled

* WIP cols auto approach

* Merge blocking dates

* Fix zindex

* Fix hover position

* Fix Z-Index issues on hover and blocking events

* Re add onclick handler

* Fix overlapping blocking dates

* Fix scaling for datevalues columns

* Date values closer to DS

* Blocked List Tidy up

* Storybook + file tidy up

* Little tidy up

* Fix offsets

* Remove event hover

* Fix random bg-red-500

* Fix import

* FIx blocking cells appearing above start Date

* Fix truncation

* Fix border overlap

* Overlap a little nicer

* Condtional 80% sizing

* Nitpicks

* Fix today height and top breaking

* Add text left to time stamp

* Support string dates

* Add shalow to reduce re-renders

* Rename to Calendar instead of scheduler

* Fix 3 overlapping events

* Fix merge type error

* Fix destructuring

* NITS

* Move to features package

Co-authored-by: Jeroen Reumkens <hello@jeroenreumkens.nl>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-14 13:36:10 +00:00
Joe Au-Yeung 2078f12a85 Outlook - list all calendars (#6004) 2022-12-14 11:04:32 +00:00
github-actions[bot]andCrowdin Bot 77b8eb8a7f New Crowdin translations by Github Action (#6018)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-14 11:57:02 +01:00
Peer Richelsenandsean-brydon 463787b607 team banner (#5956)
Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2022-12-14 09:04:45 +01:00
alannncandPeer Richelsen 1f4e7ca695 Adding new middleware and using it for test action workflow (#6017)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-13 21:34:21 +00:00
Jeroen ReumkensandPeer Richelsen 9eb52bbfb2 #5613 Optimise social images by utilising next image api. (#5997)
* #5613 Optimise social images by utilising next image api.

* removed unused import

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-13 21:22:38 +00:00
Leo GiovanettiandPeer Richelsen 61e6f76bc6 Direct links to accept/reject a booking (#5939)
* Impl

* Added reason

* Secondary reject CTA

* Update packages/emails/src/components/Info.tsx

* Adjusting recurring info

* Not showing reason for rejecting if none given

* Feedback

* Error handling + style fixes

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-13 22:09:28 +01:00
github-actions[bot]andCrowdin Bot 2e6c3a6068 New Crowdin translations by Github Action (#6001)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-13 19:11:45 +01:00
8f48ecafec Misc fixes (#5988)
* Address reports

* Clean up

* remove activeOn in where

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-13 17:44:53 +00:00
Peer Richelsen 8849d1148c removed z-index from topbanner (#6008) 2022-12-13 23:04:25 +05:30
98a4684a2b Impl (#5993)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Bailey Pumfleet <bailey@pumfleet.co.uk>
2022-12-13 10:25:30 -03:00
Omar López da0bdfacae Handle membership updates in Stripe (#5992) 2022-12-13 12:43:37 +00:00
github-actions[bot]andCrowdin Bot 68c7425922 New Crowdin translations by Github Action (#5996)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-13 11:07:00 +01:00
github-actions[bot]andCrowdin Bot c3418cbe1f New Crowdin translations by Github Action (#5990)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-13 10:57:22 +01:00
Hariom Balhara 3a641e0bc1 Feature/Embed/Add Embed ui instruction config to hide event type description (#5951)
* Add embed ui instruction to hide event type description

* Add EventTypeDetails option in Embed Snippet Generator
2022-12-13 07:23:26 +00:00
Carina Wollendorfer b7fcf6ade1 fix missing space in email (#5986) 2022-12-12 12:51:27 -07:00
github-actions[bot]andCrowdin Bot 53d160e495 New Crowdin translations by Github Action (#5984)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
2022-12-12 14:24:10 +01:00
sean-brydon df4df6846b Required radio button (#5985) 2022-12-12 14:18:04 +01:00
Carina WollendorferandCarinaWolli c1bbb9b139 Fixes SMS not sending for country codes not supporting alphanumeric sender IDs (#5969)
* don't use alphanumeric sender ID for not support country codes

* fix import

* rename file

* fix spelling mistakes

* add twilio phone number to .env file

* add SENDER_ID to env variable and create function to get sender id

* add NEXT_PUBLIC_SENDER_ID env variable

* remove not needed Cal fallback

* fix readme

* code clean up

* code clean up

Co-authored-by: CarinaWolli <wollencarina@gmail.com>
2022-12-12 12:00:15 +01:00
Hariom Balhara 56c07ec351 Hide Branding in Embed. (#5975) 2022-12-12 08:37:36 +00:00
Hariom Balhara 3080b47aec Theming shouldnt depend on plan now as everyone is pro now (#5976) 2022-12-12 08:25:59 +00:00
c1e23bfdc6 fix/duplicate-event-types (#5888)
* Add new modal for duplicate and trpc action for it

* Adding event type duplication validations

* Fix type errors and id not being removed from rest obj

* Fix test for duplicating event

* Fix test for duplicating event

* Add connect to event type workflows

* fix button for adding new events

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
2022-12-10 15:48:26 -07:00
e7ff5508d7 New Crowdin translations by Github Action (#5966)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-10 22:46:06 +01:00
Nafees Nazik 5850d227ee fix: render upgrade banner above ImpersonatingBanner (#5968) 2022-12-10 21:45:35 +00:00
sean-brydon ede987ba19 Fixing branding toggle (#5952)
* Fix form state not saving

* Always show false on toggle if you dont have an active team

* Correct filter
2022-12-09 22:33:49 +00:00
zomars bc0d83164c v2.3.8
release.yml / release (push) Canceled after 0s
2022-12-09 10:43:49 -07:00
zomars 30f0b080c5 SIU 2022-12-09 10:43:19 -07:00
Leo Giovanetti ee39ea316f Fixing app store search input (#5960) 2022-12-09 14:10:54 -03:00
Hariom BalharaandPeer Richelsen fbc2708259 Hotfix/Google Meet must be globally installed (#5948)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2022-12-09 12:51:23 +00:00
9bb83aba7e removed license consent (#5931)
Co-authored-by: alannnc <alannnc@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
2022-12-09 11:05:42 +00:00
521 changed files with 11435 additions and 11505 deletions
+5 -3
View File
@@ -7,14 +7,13 @@
# - E-MAIL SETTINGS
# - LICENSE *************************************************************************************************
# Set this value to 'agree' to accept our license:
# LICENSE: https://github.com/calendso/calendso/blob/main/LICENSE
# https://github.com/calendso/calendso/blob/main/LICENSE
#
# Summary of terms:
# - The codebase has to stay open source, whether it was modified or not
# - You can not repackage or sell the codebase
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
NEXT_PUBLIC_LICENSE_CONSENT=''
#
# To enable enterprise-only features, fill your license key in here.
# @see https://console.cal.com
CALCOM_LICENSE_KEY=
@@ -86,6 +85,9 @@ SENDGRID_EMAIL=
TWILIO_SID=
TWILIO_TOKEN=
TWILIO_MESSAGING_SID=
TWILIO_PHONE_NUMBER=
NEXT_PUBLIC_SENDER_ID=
TWILIO_VERIFY_SID=
# This is used so we can bypass emails in auth flows for E2E testing
# Set it to "1" if you need to run E2E tests locally
+7
View File
@@ -3,3 +3,10 @@
automerge:
- apps/web/public/static/locales/**/*.json
"❗️ migrations":
- packages/prisma/migrations/**/migration.sql
"❗️ .env changes":
- .env.example
- .env.appStore.example
-3
View File
@@ -12,8 +12,5 @@ jobs:
runs-on: ubuntu-latest
steps:
- uses: actions/labeler@v4
# Checking the actor will prevent your Action run failing on non-Dependabot
# PRs but also ensures that it only does work for Dependabot PRs.
if: ${{ github.actor == 'github-actions[bot]' }}
with:
repo-token: "${{ secrets.GITHUB_TOKEN }}"
+16 -13
View File
@@ -1,8 +1,7 @@
<!-- PROJECT LOGO -->
<p align="center">
<a href="https://github.com/calcom/cal.com">
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
<img src="https://user-images.githubusercontent.com/8019099/210054112-5955e812-a76e-4160-9ddd-58f2c72f1cce.png" alt="Logo">
</a>
<h3 align="center">Cal.com (formerly Calendso)</h3>
@@ -37,7 +36,7 @@
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
<a href="https://github.com/orgs/calcom/projects/9"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
@@ -292,7 +291,7 @@ You can deploy Cal on [Railway](https://railway.app/) using the button above. Th
Currently Vercel Pro Plan is required to be able to Deploy this application with Vercel, due to limitations on the number of serverless functions on the free plan.
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY,NEXT_PUBLIC_LICENSE_CONSENT&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
[![Deploy with Vercel](https://vercel.com/button)](https://vercel.com/new/clone?repository-url=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com&env=DATABASE_URL,NEXT_PUBLIC_WEBAPP_URL,NEXTAUTH_URL,NEXTAUTH_SECRET,CRON_API_KEY,CALENDSO_ENCRYPTION_KEY&envDescription=See%20all%20available%20env%20vars&envLink=https%3A%2F%2Fgithub.com%2Fcalcom%2Fcal.com%2Fblob%2Fmain%2F.env.example&project-name=cal&repo-name=cal.com&build-command=cd%20../..%20%26%26%20yarn%20build&root-directory=apps%2Fweb%2F)
<!-- ROADMAP -->
@@ -310,7 +309,7 @@ Please see our [contributing guide](/CONTRIBUTING.md).
### Good First Issues
We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/25) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
We have a list of [help wanted](https://github.com/calcom/cal.com/issues?q=is:issue+is:open+label:%22%F0%9F%99%8B%F0%9F%8F%BB%E2%80%8D%E2%99%82%EF%B8%8Fhelp+wanted%22) that contain small features and bugs which have a relatively limited scope. This is a great place to get started, gain experience, and get familiar with our contribution process.
## Integrations
@@ -419,14 +418,18 @@ following
2. Click ‘Get a Twilio phone number’
3. Copy Account SID to your .env file into the TWILIO_SID field
4. Copy Auth Token to your .env file into the TWILIO_TOKEN field
5. Create a messaging service (Develop -> Messaging -> Services)
6. Choose any name for the messaging service
7. Click 'Add Senders'
8. Choose phone number as sender type
9. Add the listed phone number
10. Leave all other fields as they are
11. Complete setup and click ‘View my new Messaging Service’
12. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
5. Copy your Twilio phone number to your .env file into the TWILIO_PHONE_NUMBER field
6. Add your own sender id to the .env file into the NEXT_PUBLIC_SENDER_ID field (fallback is Cal)
7. Create a messaging service (Develop -> Messaging -> Services)
8. Choose any name for the messaging service
9. Click 'Add Senders'
10. Choose phone number as sender type
11. Add the listed phone number
12. Leave all other fields as they are
13. Complete setup and click ‘View my new Messaging Service’
14. Copy Messaging Service SID to your .env file into the TWILIO_MESSAGING_SID field
15. Create a verify service
16. Copy Verify Service SID to your .env file into the TWILIO_VERIFY_SID field
<!-- LICENSE -->
-5
View File
@@ -33,11 +33,6 @@
"generator": "secret",
"required": "true"
},
"NEXT_PUBLIC_LICENSE_CONSENT": {
"description": "License consent",
"value": "true",
"required": "true"
},
"CRON_API_KEY": {
"description": "ApiKey for cronjobs",
"value": ""
+1 -2
View File
@@ -14,14 +14,13 @@ Cal.com uses [TailwindCSS](https://tailwindcss.com) as a replacement for traditi
CSS files should be stored in the `styles` directory within the codebase.
Within the `styles` directory, you will find two CSS files, `fonts.css` and `global.css`. We suggest not to add to these files, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
Within the `styles` directory, you will find a single CSS file, `global.css`. We suggest not to add to this file, and instead create new CSS files and import them into the application. This helps reduce conflicts when pulling in changes that we've made to either of the existing CSS files.
## Adding new stylesheets
Firstly, create the CSS file inside the `styles` directory.
Then, open the `pages/_app.tsx` file, and you will see the following two lines:
```javascript
import "../styles/fonts.css";
import "../styles/globals.css";
```
+16
View File
@@ -4,6 +4,7 @@ module.exports = {
stories: [
"../intro.stories.mdx",
"../../../packages/ui/components/**/*.stories.mdx",
"../../../packages/features/**/*.stories.mdx",
"../../../packages/ui/components/**/*.stories.@(js|jsx|ts|tsx)",
],
addons: [
@@ -50,6 +51,21 @@ module.exports = {
vm: false,
zlib: false,
};
config.module.rules.push({
test: /\.css$/,
use: [
"style-loader",
{
loader: "css-loader",
options: {
modules: true, // Enable modules to help you using className
},
},
],
include: path.resolve(__dirname, "../src"),
});
return config;
},
};
+5 -1
View File
@@ -15,7 +15,11 @@ export const parameters = {
},
};
addDecorator((storyFn) => <I18nextProvider i18n={i18n}>{storyFn()}</I18nextProvider>);
addDecorator((storyFn) => (
<I18nextProvider i18n={i18n}>
<div style={{ margin: "2rem" }}>{storyFn()}</div>
</I18nextProvider>
));
window.getEmbedNamespace = () => {
const url = new URL(document.URL);
+1 -1
View File
@@ -2,4 +2,4 @@
@tailwind components;
@tailwind utilities;
@import "../../../packages/ui/styles/shared-globals.css"
@import "../../../packages/ui/styles/shared-globals.css"
+7 -1
View File
@@ -1,3 +1,5 @@
@import url("../../../packages/features/calendars/weeklyview/styles/styles.css");
.sbdocs {
font-family: 'Inter var' !important;
padding: 0!important;
@@ -36,7 +38,7 @@
/** Docs table **/
.custom-args-wrapper{
max-height: 600px;
max-height: 400px;
overflow-y: scroll;
overflow-x: hidden;
margin-bottom: 1rem;
@@ -187,3 +189,7 @@
padding: 24px !important;
}
}
.docs-story {
padding: 24px !important;
}
+26 -6
View File
@@ -44,6 +44,7 @@ type PreviewState = {
palette: {
brandColor: string;
};
hideEventTypeDetails: boolean;
};
const queryParamsForDialog = ["embedType", "embedTabName", "embedUrl"];
@@ -125,10 +126,12 @@ const getEmbedUIInstructionString = ({
apiName,
theme,
brandColor,
hideEventTypeDetails,
}: {
apiName: string;
theme?: string;
brandColor: string;
hideEventTypeDetails: boolean;
}) => {
theme = theme !== "auto" ? theme : undefined;
return getInstructionString({
@@ -141,6 +144,7 @@ const getEmbedUIInstructionString = ({
brandColor,
},
},
hideEventTypeDetails: hideEventTypeDetails,
},
});
};
@@ -253,18 +257,21 @@ const getEmbedTypeSpecificString = ({
apiName: string;
theme: PreviewState["theme"];
brandColor: string;
hideEventTypeDetails: boolean;
};
if (embedFramework === "react") {
uiInstructionStringArg = {
apiName: "cal",
theme: previewState.theme,
brandColor: previewState.palette.brandColor,
hideEventTypeDetails: previewState.hideEventTypeDetails,
};
} else {
uiInstructionStringArg = {
apiName: "Cal",
theme: previewState.theme,
brandColor: previewState.palette.brandColor,
hideEventTypeDetails: previewState.hideEventTypeDetails,
};
}
if (!frameworkCodes[embedType]) {
@@ -632,7 +639,7 @@ const ChooseEmbedTypesDialogContent = () => {
<div className="flex items-start">
{embeds.map((embed, index) => (
<button
className="mr-2 w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100"
className="w-1/3 border border-transparent p-3 text-left hover:rounded-md hover:border-gray-200 hover:bg-neutral-100 ltr:mr-2 rtl:ml-2"
key={index}
data-testid={embed.type}
onClick={() => {
@@ -689,6 +696,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
theme: Theme.auto,
floatingPopup: {},
elementClick: {},
hideEventTypeDetails: false,
palette: {
brandColor: "#000000",
},
@@ -753,6 +761,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
name: "ui",
arg: {
theme: previewState.theme,
hideEventTypeDetails: previewState.hideEventTypeDetails,
styles: {
branding: {
...previewState.palette,
@@ -811,7 +820,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
{embed.title}
</h3>
<hr className={classNames("mt-4", embedType === "element-click" ? "hidden" : "")} />
<div className="max-h-97 flex flex-col overflow-y-auto">
<div className="flex flex-col overflow-y-auto">
<div className={classNames("mt-4 font-medium", embedType === "element-click" ? "hidden" : "")}>
<Collapsible
open={isEmbedCustomizationOpen}
@@ -838,7 +847,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className="text-sm">Embed Window Sizing</div>
<div className="justify-left flex items-center">
<TextField
name="width"
labelProps={{ className: "hidden" }}
required
value={previewState.inline.width}
@@ -860,7 +868,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<span className="p-2">×</span>
<TextField
labelProps={{ className: "hidden" }}
name="height"
value={previewState.inline.height}
required
onChange={(e) => {
@@ -888,7 +895,6 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div className="mb-2 text-sm">Button Text</div>
{/* Default Values should come from preview iframe */}
<TextField
name="buttonText"
labelProps={{ className: "hidden" }}
onChange={(e) => {
setPreviewState((previewState) => {
@@ -908,7 +914,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
<div
className={classNames(
"mt-4 flex items-center justify-start",
embedType === "floating-popup" ? "space-x-2" : "hidden"
embedType === "floating-popup" ? "space-x-2 rtl:space-x-reverse" : "hidden"
)}>
<Switch
defaultChecked={true}
@@ -1004,6 +1010,20 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => {
setPreviewState((previewState) => {
return {
...previewState,
hideEventTypeDetails: checked,
};
});
}}
/>
<div className="text-sm">{t("hide_eventtype_details")}</div>
</div>
<Label className="">
<div className="mb-2">Theme</div>
<Select
+6 -1
View File
@@ -1,6 +1,7 @@
import { useTranslation } from "next-i18next";
import { useEffect } from "react";
import { getDirFromLang } from "@calcom/lib/i18n";
import { trpc } from "@calcom/trpc/react";
export function useViewerI18n() {
@@ -21,12 +22,16 @@ export function useViewerI18n() {
*/
const I18nLanguageHandler = (): null => {
const { i18n } = useTranslation("common");
const locale = useViewerI18n().data?.locale;
const locale = useViewerI18n().data?.locale || "en";
useEffect(() => {
if (locale && i18n.language && i18n.language !== locale) {
if (typeof i18n.changeLanguage === "function") i18n.changeLanguage(locale);
}
const dir = getDirFromLang(locale);
document.documentElement.setAttribute("lang", locale);
document.documentElement.setAttribute("dir", dir);
}, [locale, i18n]);
return null;
+3 -1
View File
@@ -66,7 +66,9 @@ const Component = ({
}
);
const allowedMultipleInstalls = categories.indexOf("calendar") > -1;
// 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";
return (
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
@@ -15,7 +15,7 @@ import {
List,
ShellSubHeading,
showToast,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
Switch,
} from "@calcom/ui";
@@ -159,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
if (!data.connectedCalendars.length) {
return null;
}
return (
<List className="flex flex-col gap-6" noBorderTreatment>
{data.connectedCalendars.map((item) => (
@@ -168,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
slug={item.integration.slug}
title={item.integration.title}
logo={item.integration.logo}
description={item.primary?.externalId || "No external Id"}
description={item.primary?.email ?? item.integration.description}
separate={true}
actions={
<div className="flex w-32 justify-end">
@@ -44,7 +44,7 @@ function IntegrationListItem(props: {
expanded={!!props.children}
className={classNames(
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
"my-0 flex-col border transition-colors duration-500 ",
"my-0 flex-col border transition-colors duration-500",
highlight ? "bg-yellow-100" : ""
)}>
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
@@ -56,7 +56,7 @@ function IntegrationListItem(props: {
<ListItemText component="p">{props.description}</ListItemText>
{/* Alert error that key stopped working. */}
{props.invalidCredential && (
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 rtl:space-x-reverse">
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
{t("invalid_credential")}
@@ -1,10 +1,13 @@
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
import { classNames } from "@calcom/lib";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon, Tooltip } from "@calcom/ui";
import { Props } from "./pages/AvailabilityPage";
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
const { t } = useLocale();
return locations.length ? (
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
{locations.map((location) => {
@@ -16,19 +19,19 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
return (
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
{eventLocationType.iconUrl === "/link.svg" ? (
<Icon.FiLink className="dark:text-darkgray-600 mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 " />
<Icon.FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
) : (
<img
src={eventLocationType.iconUrl}
className={classNames(
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
)}
alt={`${eventLocationType.label} icon`}
/>
)}
<Tooltip content={locationKeyToString(location)}>
<p className="truncate">{locationKeyToString(location)}</p>
<Tooltip content={t(locationKeyToString(location) ?? "")}>
<p className="truncate">{t(locationKeyToString(location) ?? "")}</p>
</Tooltip>
</div>
);
@@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
</span>
<span className="text-bookinglight font-medium">
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
</span>
</div>
<div className="ml-auto">
@@ -71,7 +71,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
/>
</div>
</div>
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
{slots.length > 0 &&
slots.map((slot) => {
type BookingURL = {
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
}
return (
<div key={dayjs(slot.time).format()}>
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
{/* Current there is no way to disable Next.js Links */}
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
<div
@@ -44,18 +44,17 @@ const BookingDescription: FC<Props> = (props) => {
const { profile, eventType, isBookingPage = false, children } = props;
const { date: bookingDate } = useRouterQuery("date");
const { t } = useLocale();
const { duration, setQuery: setDuration } = useRouterQuery("duration");
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
useEffect(() => {
if (eventType.metadata?.multipleDuration !== undefined) {
if (!duration) {
setDuration(eventType.length);
} else {
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
setDuration(eventType.length);
}
}
if (
eventType.metadata?.multipleDuration &&
!eventType.metadata?.multipleDuration?.includes(Number(duration))
) {
setDuration(eventType.length.toString());
}
}, []);
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
let requiresConfirmation = eventType?.requiresConfirmation;
let requiresConfirmationText = t("requires_confirmation");
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
@@ -96,7 +95,7 @@ const BookingDescription: FC<Props> = (props) => {
<div>
<Icon.FiInfo
className={classNames(
"mr-[10px] ml-[2px] inline-block h-4 w-4",
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
)}
/>
@@ -113,7 +112,7 @@ const BookingDescription: FC<Props> = (props) => {
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
)}>
<div>
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
</div>
{requiresConfirmationText}
</div>
@@ -128,7 +127,7 @@ const BookingDescription: FC<Props> = (props) => {
)}>
<Icon.FiClock
className={classNames(
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
isBookingPage && "mt-[2px]"
)}
/>
+35 -21
View File
@@ -4,6 +4,7 @@ import { useState } from "react";
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
import dayjs from "@calcom/dayjs";
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
import classNames from "@calcom/lib/classNames";
import { formatTime } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -22,15 +23,16 @@ import {
showToast,
TextArea,
Tooltip,
ActionType,
TableActions,
} from "@calcom/ui";
import useMeQuery from "@lib/hooks/useMeQuery";
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
import TableActions, { ActionType } from "@components/ui/TableActions";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
@@ -48,6 +50,7 @@ function BookingListItem(booking: BookingItemProps) {
const router = useRouter();
const [rejectionReason, setRejectionReason] = useState<string>("");
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
const mutation = trpc.viewer.bookings.confirm.useMutation({
onSuccess: (data) => {
if (data.status === BookingStatus.REJECTED) {
@@ -112,6 +115,17 @@ function BookingListItem(booking: BookingItemProps) {
},
];
const showRecordingActions: ActionType[] = [
{
id: "view_recordings",
label: t("view_recordings"),
onClick: () => {
setViewRecordingsDialogIsOpen(true);
},
disabled: mutation.isLoading,
},
];
let bookedActions: ActionType[] = [
{
id: "cancel",
@@ -158,6 +172,10 @@ function BookingListItem(booking: BookingItemProps) {
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
}
if (isPast && isPending && !isConfirmed) {
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
}
const RequestSentMessage = () => {
return (
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
@@ -202,7 +220,7 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
return (
<>
<RescheduleDialog
@@ -216,7 +234,14 @@ function BookingListItem(booking: BookingItemProps) {
isOpenDialog={isOpenSetLocationDialog}
setShowLocationModal={setIsOpenLocationDialog}
/>
{showRecordingsButtons && (
<ViewRecordingsDialog
booking={booking}
isOpenDialog={viewRecordingsDialogIsOpen}
setIsOpenDialog={setViewRecordingsDialogIsOpen}
timeFormat={user?.timeFormat ?? null}
/>
)}
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
<DialogContent>
@@ -248,7 +273,7 @@ function BookingListItem(booking: BookingItemProps) {
</DialogContent>
</Dialog>
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
<td
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
onClick={onClickTableData}>
@@ -275,11 +300,6 @@ function BookingListItem(booking: BookingItemProps) {
{booking.eventType.team.name}
</Badge>
)}
{!!booking?.eventType?.price && !booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
{t("pending_payment")}
</Badge>
)}
{booking.paid && (
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
{t("paid")}
@@ -343,12 +363,14 @@ function BookingListItem(booking: BookingItemProps) {
<span> </span>
{!!booking?.eventType?.price && !booking.paid && (
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
</div>
{booking.description && (
<div
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
title={booking.description}>
&quot;{booking.description}&quot;
</div>
@@ -376,6 +398,7 @@ function BookingListItem(booking: BookingItemProps) {
</>
) : null}
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
{isCancelled && booking.rescheduled && (
<div className="hidden h-full items-center md:flex">
<RequestSentMessage />
@@ -531,13 +554,4 @@ const DisplayAttendees = ({
);
};
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
return (
<span
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
{children}
</span>
);
};
export default BookingListItem;
@@ -13,9 +13,11 @@ import dayjs, { Dayjs } from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedStyles,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import DatePicker from "@calcom/features/calendars/DatePicker";
import CustomBranding from "@calcom/lib/CustomBranding";
import classNames from "@calcom/lib/classNames";
import getStripeAppData from "@calcom/lib/getStripeAppData";
@@ -26,7 +28,7 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
import { trpc } from "@calcom/trpc/react";
import { Icon, DatePicker } from "@calcom/ui";
import { Icon } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -150,7 +152,10 @@ const SlotPicker = ({
eventTypeId: eventType.id,
eventTypeSlug: eventType.slug,
usernameList: users,
startTime: browsingDate?.startOf("month"),
startTime:
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
: browsingDate?.startOf("month"),
endTime: browsingDate?.endOf("month"),
timeZone,
duration,
@@ -216,7 +221,7 @@ function TimezoneDropdown({
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
<p className="flex items-center text-sm font-medium">
<Icon.FiGlobe className="min-h-4 min-w-4 mr-[10px] ml-[2px] -mt-[2px] inline-block" />
<Icon.FiGlobe className="min-h-4 min-w-4 ml-[2px] -mt-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]" />
{timeZone}
{isTimeOptionsOpen ? (
<Icon.FiChevronUp className="min-h-4 min-w-4 ml-1 inline-block" />
@@ -253,6 +258,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
useTheme(profile.theme);
const { t } = useLocale();
const availabilityDatePickerEmbedStyles = useEmbedStyles("availabilityDatePicker");
//TODO: Plan to remove shouldAlignCentrallyInEmbed config
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const isBackgroundTransparent = useIsBackgroundTransparent();
@@ -289,7 +295,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
);
}
}, [telemetry]);
const embedUiConfig = useEmbedUiConfig();
// get dynamic user list here
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
@@ -302,6 +308,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
const rawSlug = profile.slug ? profile.slug.split("/") : [];
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
// Define conditional gates here
const gates = [
// Rainbow gate is only added if the event has both a `blockchainId` and a `smartContractAddress`
@@ -335,7 +343,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<div>
<main
className={classNames(
"flex-col md:mx-4 lg:flex",
"flex flex-col md:mx-4",
shouldAlignCentrally ? "items-center" : "items-start",
!isEmbed && classNames("mx-auto my-0 ease-in-out md:my-24")
)}>
@@ -350,54 +358,55 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
isEmbed && "mx-auto"
)}>
<div className="overflow-hidden md:flex">
<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]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<Icon.FiRefreshCcw className="float-left mr-[10px] mt-[7px] ml-[2px] inline-block h-4 w-4 " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
{showEventTypeDetails && (
<div
className={classNames(
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
"min-w-full md:w-[230px] md:min-w-[230px]",
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
)}>
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
{!rescheduleUid && eventType.recurringEvent && (
<div className="flex items-start text-sm font-medium">
<Icon.FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
<div>
<p className="mb-1 -ml-2 inline px-2 py-1">
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
</p>
<input
type="number"
min="1"
max={eventType.recurringEvent.count}
className="w-15 dark:bg-darkgray-200 h-7 rounded-sm border-gray-300 bg-white text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2 dark:border-gray-500"
defaultValue={eventType.recurringEvent.count}
onChange={(event) => {
setRecurringEventCount(parseInt(event?.target.value));
}}
/>
<p className="inline">
{t("occurrence", {
count: recurringEventCount,
})}
</p>
</div>
</div>
</div>
)}
{stripeAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
{timezoneDropdown}
</BookingDescription>
)}
{stripeAppData.price > 0 && (
<p className="-ml-2 px-2 text-sm font-medium">
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
{timezoneDropdown}
</BookingDescription>
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
{/* Temporarily disabled - booking?.startTime && rescheduleUid && (
<div>
<p
className="mt-4 mb-3 text-gray-600 dark:text-darkgray-600"
@@ -405,12 +414,13 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
{t("former_time")}
</p>
<p className="text-gray-500 line-through dark:text-darkgray-600">
<CalendarIcon className="mr-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
<CalendarIcon className="ltr:mr-[10px] rtl:ml-[10px] -mt-1 inline-block h-4 w-4 text-gray-500" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)*/}
</div>
</div>
)}
<SlotPicker
weekStart={
typeof profile.weekStart === "string"
@@ -436,7 +446,8 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
/>
</div>
</div>
{(!restProps.isBrandingHidden || isEmbed) && <PoweredByCal />}
{/* FIXME: We don't show branding in Embed yet because we need to place branding on top of the main content. Keeping it outside the main content would have visibility issues because outside main content background is transparent */}
{!restProps.isBrandingHidden && !isEmbed && <PoweredByCal />}
</div>
</main>
</div>
+196 -121
View File
@@ -26,6 +26,7 @@ import { LocationObject, LocationType } from "@calcom/core/location";
import dayjs from "@calcom/dayjs";
import {
useEmbedNonStylesConfig,
useEmbedUiConfig,
useIsBackgroundTransparent,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
@@ -89,6 +90,7 @@ const BookingPage = ({
const { t, i18n } = useLocale();
const { duration: queryDuration } = useRouterQuery("duration");
const isEmbed = useIsEmbed(restProps.isEmbed);
const embedUiConfig = useEmbedUiConfig();
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
const router = useRouter();
@@ -141,6 +143,7 @@ const BookingPage = ({
isSuccessBookingPage: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
},
});
},
@@ -156,6 +159,7 @@ const BookingPage = ({
allRemainingBookings: true,
email: bookingForm.getValues("email"),
eventTypeSlug: eventType.slug,
formerTime: booking?.startTime.toString(),
},
});
},
@@ -297,6 +301,39 @@ const BookingPage = ({
}
const bookEvent = (booking: BookingFormValues) => {
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
}));
// Checking if custom inputs of type Phone number are valid to display error message on UI
if (eventType.customInputs.length) {
let isErrorFound = false;
eventType.customInputs.forEach((customInput) => {
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
try {
z.string({
errorMap: () => ({
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
}),
})
.refine((val) => isValidPhoneNumber(val), {
message: "Phone number is invalid",
})
.parse(input?.value);
} catch (err) {
isErrorFound = true;
bookingForm.setError(`customInputs.${customInput.id}`, {
type: "custom",
message: "Invalid Phone number",
});
}
}
});
if (isErrorFound) return;
}
telemetry.event(
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
{ isTeamBooking: document.URL.includes("team/") }
@@ -315,6 +352,22 @@ const BookingPage = ({
{}
);
if (eventType.customInputs.length > 0) {
// find all required custom inputs and ensure they are filled out in the booking form
const requiredCustomInputs = eventType.customInputs.filter((input) => input.required);
const missingRequiredCustomInputs = requiredCustomInputs.filter(
(input) => !booking?.customInputs?.[input.id]
);
if (missingRequiredCustomInputs.length > 0) {
missingRequiredCustomInputs.forEach((input) => {
bookingForm.setError(`customInputs.${input.id}`, {
type: "required",
});
});
return;
}
}
if (recurringDates.length) {
// Identify set of bookings to one intance of recurring event to support batch changes
const recurringEventId = uuidv4();
@@ -337,10 +390,7 @@ const BookingPage = ({
attendeeAddress: booking.attendeeAddress,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
})),
customInputs: bookingCustomInputs,
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
@@ -368,10 +418,7 @@ const BookingPage = ({
attendeeAddress: booking.attendeeAddress,
}),
metadata,
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
})),
customInputs: bookingCustomInputs,
hasHashedBookingLink,
hashedLink,
smsReminderNumber:
@@ -406,6 +453,7 @@ const BookingPage = ({
}
});
}
const showEventTypeDetails = (isEmbed && !embedUiConfig.hideEventTypeDetails) || !isEmbed;
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
// Define conditional gates here
@@ -439,7 +487,7 @@ const BookingPage = ({
className={classNames(
shouldAlignCentrally ? "mx-auto" : "",
isEmbed ? "" : "sm:my-24",
"my-0 max-w-3xl "
"my-0 max-w-3xl"
)}>
<div
className={classNames(
@@ -448,95 +496,97 @@ const BookingPage = ({
"dark:border-darkgray-300 rounded-md sm:border"
)}>
<div className="sm:flex">
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<Icon.FiCreditCard className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
)}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
<Icon.FiRefreshCw className="mr-[10px] ml-[2px] inline-block h-4 w-4" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
recurringEvent: eventType.recurringEvent,
recurringCount: recurringEventCount,
})}
{showEventTypeDetails && (
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
{stripeAppData.price > 0 && (
<p className="text-bookinglight -ml-2 px-2 text-sm ">
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<IntlProvider locale="en">
<FormattedNumber
value={stripeAppData.price / 100.0}
style="currency"
currency={stripeAppData.currency.toUpperCase()}
/>
</IntlProvider>
</p>
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
<div className="text-sm font-medium">
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
{!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
return <p key={key}>{timeFormatted}</p>;
})}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
)}
</div>
</div>
{booking?.startTime && rescheduleUid && (
<div>
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
{t("former_time")}
</p>
<p className="line-through ">
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
{!!eventType.seatsPerTimeSlot && (
)}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
<Icon.FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<p className="-ml-2 inline-block items-center px-2">
{getEveryFreqFor({
t,
recurringEvent: eventType.recurringEvent,
recurringCount: recurringEventCount,
})}
</p>
</div>
)}
<div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiUser
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
} mb-2 font-medium`}>
{booking
? eventType.seatsPerTimeSlot - booking.attendees.length
: eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
</p>
<Icon.FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
<div className="text-sm font-medium">
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
{!rescheduleUid &&
eventType.recurringEvent?.freq &&
recurringStrings.slice(0, 5).map((timeFormatted, key) => {
return <p key={key}>{timeFormatted}</p>;
})}
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
<div className="flex">
<Tooltip
content={recurringStrings.slice(5).map((timeFormatted, key) => (
<p key={key}>{timeFormatted}</p>
))}>
<p className="dark:text-darkgray-600 text-sm">
+ {t("plus_more", { count: recurringStrings.length - 5 })}
</p>
</Tooltip>
</div>
)}
</div>
</div>
)}
</BookingDescription>
</div>
<div className="p-6 sm:w-1/2">
{booking?.startTime && rescheduleUid && (
<div>
<p className="mt-8 mb-2 text-sm " data-testid="former_time_p">
{t("former_time")}
</p>
<p className="line-through ">
<Icon.FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
</p>
</div>
)}
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiUser
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
}`}
/>
<p
className={`${
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
? "text-rose-600"
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
? "text-yellow-500"
: "text-bookinghighlight"
} mb-2 font-medium`}>
{booking
? eventType.seatsPerTimeSlot - booking.attendees.length
: eventType.seatsPerTimeSlot}{" "}
/ {eventType.seatsPerTimeSlot} {t("seats_available")}
</p>
</div>
)}
</BookingDescription>
</div>
)}
<div className={classNames("p-6", showEventTypeDetails ? "sm:w-1/2" : "w-full")}>
<Form form={bookingForm} handleSubmit={bookEvent}>
<div className="mb-4">
<label htmlFor="name" className="block text-sm font-medium text-gray-700 dark:text-white">
@@ -573,7 +623,7 @@ const BookingPage = ({
/>
{bookingForm.formState.errors.email && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="mr-2 h-3 w-3" />
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("email_validation_error")}</p>
</div>
)}
@@ -614,8 +664,8 @@ const BookingPage = ({
value={location.type}
defaultChecked={i === 0}
/>
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
{locationKeyToString(location)}
<span className="text-sm ltr:ml-2 ltr:mr-2 rtl:ml-2 dark:text-white">
{t(locationKeyToString(location) ?? "")}
</span>
</label>
);
@@ -639,7 +689,7 @@ const BookingPage = ({
{selectedLocationType === LocationType.Phone
? t("phone_number")
: selectedLocationType === LocationType.AttendeeInPerson
? t("Address")
? t("address")
: ""}
</label>
<div className="mt-1">
@@ -666,7 +716,7 @@ const BookingPage = ({
</div>
{bookingForm.formState.errors.phone && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="mr-2 h-3 w-3" />
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p>
</div>
)}
@@ -679,8 +729,11 @@ const BookingPage = ({
{input.type !== EventTypeCustomInputType.BOOL && (
<label
htmlFor={"custom_" + input.id}
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
{input.label}
className={classNames(
"mb-1 block text-sm font-medium text-gray-700 transition-colors dark:text-white",
bookingForm.formState.errors.customInputs?.[input.id] && "!text-red-700"
)}>
{input.label} {input.required && <span className="text-red-700">*</span>}
</label>
)}
{input.type === EventTypeCustomInputType.TEXTLONG && (
@@ -745,24 +798,46 @@ const BookingPage = ({
</div>
)}
{input.options && input.type === EventTypeCustomInputType.RADIO && (
<div className="">
<div className="flex">
<Group
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
}}>
<>
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
value={option.label}
id={`option.${i}.radio`}
/>
))}
</>
</Group>
</div>
<div className="flex">
<Group
name={`customInputs.${input.id}`}
required={input.required}
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
}}>
<>
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${input.id}.${i}.radio`}
value={option.label}
id={`option.${input.id}.${i}.radio`}
/>
))}
</>
{bookingForm.formState.errors.customInputs?.[input.id] && (
<div className="mt-px flex items-center text-xs text-red-700 ">
<p>{t("required")}</p>
</div>
)}
</Group>
</div>
)}
{input.type === EventTypeCustomInputType.PHONE && (
<div>
<PhoneInput<BookingFormValues>
name={`customInputs.${input.id}`}
control={bookingForm.control}
placeholder={t("enter_phone_number")}
id={`customInputs.${input.id}`}
required={input.required}
/>
{bookingForm.formState.errors?.customInputs?.[input.id] && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p>
</div>
)}
</div>
)}
</div>
@@ -839,7 +914,7 @@ const BookingPage = ({
</div>
{bookingForm.formState.errors.smsReminderNumber && (
<div className="mt-2 flex items-center text-sm text-red-700 ">
<Icon.FiInfo className="mr-2 h-3 w-3" />
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
<p>{t("invalid_number")}</p>
</div>
)}
@@ -354,7 +354,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
/>
{selectedLocation && LocationOptions}
<DialogFooter>
<div className="mt-4 flex justify-end space-x-2">
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
<Button
onClick={() => {
setShowLocationModal(false);
@@ -149,7 +149,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
<span className="w-16 sm:w-28 sm:text-left">
{format(dayRange.startTime, timeFormat === 12)}
</span>
<span className="ml-4">-</span>
<span className="ltr:ml-4 rtl:mr-4">-</span>
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
</div>
))}
@@ -164,7 +164,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
<hr />
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
<Icon.FiGlobe className="mr-2" />
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" />
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
</span>
<Button
@@ -40,6 +40,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
value: EventTypeCustomInputType.RADIO,
label: t("radio"),
},
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
];
const { selectedCustomInput } = props;
@@ -133,8 +134,8 @@ const CustomInputTypeForm: FC<Props> = (props) => {
value={selectedCustomInput?.id || -1}
{...register("id", { valueAsNumber: true })}
/>
<div className="mt-5 flex justify-end space-x-2 sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
{t("cancel")}
</Button>
<Button
@@ -1,18 +1,19 @@
import Link from "next/link";
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useEffect, useState } from "react";
import { Controller, useFormContext } from "react-hook-form";
import short from "short-uuid";
import { v5 as uuidv5 } from "uuid";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Badge,
Button,
Checkbox,
CustomInputItem,
Dialog,
DialogClose,
DialogContent,
@@ -36,7 +37,19 @@ const generateHashedLink = (id: number) => {
return uid;
};
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
const getRandomId = (length = 8) => {
return (
-1 *
parseInt(
Math.ceil(Math.random() * Date.now())
.toPrecision(length)
.toString()
.replace(".", "")
)
);
};
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
@@ -51,7 +64,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
const removeCustom = (index: number) => {
@@ -64,6 +76,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]);
useEffect(() => {
if (eventType.customInputs) {
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
}
}, [eventType.customInputs]);
return (
<div className="flex flex-col space-y-8">
{/**
@@ -239,13 +257,30 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div className="mt-2 flex">
<Checkbox
description={t("disable_success_page")}
// Disable if it's not Self Hosted or if the redirect url is not set
disabled={!IS_SELF_HOSTED || !formMethods.watch("successRedirectUrl")}
{...formMethods.register("metadata.disableSuccessPage")}
/>
{/*TODO: Extract it out into a component when used more than once*/}
{!IS_SELF_HOSTED && (
<Link href="https://cal.com/pricing">
<a target="_blank">
<Badge variant="orange" className="ml-2">
Platform Only
</Badge>
</a>
</Link>
)}
</div>
</div>
</SettingsToggle>
</>
)}
/>
<hr />
<SettingsToggle
data-testid="hashedLinkCheck"
title={t("private_link")}
@@ -374,7 +409,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
<Controller
name="customInputs"
control={formMethods.control}
defaultValue={eventType.customInputs.sort((a, b) => a.id - b.id) || []}
defaultValue={customInputs}
render={() => (
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
<DialogContent
@@ -386,13 +421,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
selectedCustomInput={selectedCustomInput}
onSubmit={(values) => {
const customInput: CustomInputParsed = {
id: -1,
id: getRandomId(),
eventTypeId: -1,
label: values.label,
placeholder: values.placeholder,
required: values.required,
type: values.type,
options: values.options,
hasToBeCreated: true,
};
if (selectedCustomInput) {
selectedCustomInput.label = customInput.label;
@@ -400,6 +436,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
selectedCustomInput.required = customInput.required;
selectedCustomInput.type = customInput.type;
selectedCustomInput.options = customInput.options || undefined;
selectedCustomInput.hasToBeCreated = false;
// Update by id
const inputIndex = customInputs.findIndex((input) => input.id === values.id);
customInputs[inputIndex] = selectedCustomInput;
@@ -1,4 +1,4 @@
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useFormContext } from "react-hook-form";
import EventTypeAppContext, { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppContext";
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Button, EmptyScreen, ErrorBoundary, Icon } from "@calcom/ui";
type EventType = Pick<EventTypeSetupInfered, "eventType">["eventType"] &
type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
EventTypeAppCardComponentProps["eventType"];
function AppCardWrapper({
@@ -1,6 +1,6 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import * as RadioGroup from "@radix-ui/react-radio-group";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useRef, useState } from "react";
import { Controller, useFormContext, useWatch } from "react-hook-form";
@@ -12,7 +12,7 @@ import { PeriodType } from "@calcom/prisma/client";
import type { BookingLimit } from "@calcom/types/Calendar";
import { Button, DateRangePicker, Icon, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const minimumBookingNoticeType = useRef<DurationType>(findDurationType(eventType.minimumBookingNotice));
@@ -197,7 +197,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
label={t("minimum_booking_notice")}
type="number"
placeholder="120"
className="mr-2 mb-0 h-[38px] rounded-[4px]"
className="mb-0 h-[38px] rounded-[4px] ltr:mr-2 rtl:ml-2"
{...formMethods.register("minimumBookingNoticeInDurationType", {
onChange: (event: React.ChangeEvent<HTMLInputElement>) =>
onMinimumNoticeChange(event.target.value),
@@ -327,7 +327,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
</div>
)}
{period.type === "RANGE" && (
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
<div className="inline-flex space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
<Controller
name="periodDates"
control={formMethods.control}
@@ -347,7 +347,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupInfered, "event
/>
</div>
)}
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2">&nbsp;{period.suffix}</span> : null}
{period.suffix ? (
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2">&nbsp;{period.suffix}</span>
) : null}
</div>
);
})}
@@ -410,7 +412,9 @@ const BookingLimits = () => {
.map(([key, bookingAmount]) => {
const bookingLimitKey = key as BookingLimitsKey;
return (
<div className="mb-2 flex items-center space-x-2 text-sm" key={bookingLimitKey}>
<div
className="mb-2 flex items-center space-x-2 text-sm rtl:space-x-reverse"
key={bookingLimitKey}>
<Input
id={`${bookingLimitKey}-limit`}
type="number"
@@ -1,11 +1,10 @@
import { EventTypeSetupInfered } from "pages/event-types/[type]";
import { useState } from "react";
import { EventTypeSetupProps } from "pages/event-types/[type]";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import RecurringEventController from "./RecurringEventController";
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupInfered, "eventType">) => {
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
const stripeAppData = getStripeAppData(eventType);
const requirePayment = stripeAppData.price > 0;
@@ -1,7 +1,7 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { zodResolver } from "@hookform/resolvers/zod";
import { isValidPhoneNumber } from "libphonenumber-js";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { Controller, useForm, useFormContext } from "react-hook-form";
import { MultiValue } from "react-select";
@@ -23,7 +23,7 @@ type OptionTypeBase = {
};
export const EventSetupTab = (
props: Pick<EventTypeSetupInfered, "eventType" | "locationOptions" | "team" | "teamMembers">
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
) => {
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
@@ -169,7 +169,7 @@ export const EventSetupTab = (
alt={`${eventLocationType.label} logo`}
/>
<span className="truncate text-sm ltr:ml-1 rtl:mr-1">
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
{t(location[eventLocationType.defaultValueVariable] || eventLocationType.label)}
</span>
</div>
<div className="flex">
@@ -262,12 +262,14 @@ export const EventSetupTab = (
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
if (newOptions.length > 0) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
} else {
setDefaultDuration(null);
}
}
if (newOptions.length === 1 && defaultDuration === null) {
setDefaultDuration(newOptions[0]);
formMethods.setValue("length", newOptions[0].value);
}
formMethods.setValue("metadata.multipleDuration", values);
}}
@@ -288,7 +290,7 @@ export const EventSetupTab = (
setDefaultDuration(
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
);
formMethods.setValue("length", Number(option?.value));
if (option) formMethods.setValue("length", option.value);
}}
/>
</div>
@@ -296,14 +298,11 @@ export const EventSetupTab = (
) : (
<TextField
required
name="length"
type="number"
label={t("duration")}
addOnSuffix={<>{t("minutes")}</>}
defaultValue={eventType.length ?? 15}
onChange={(e) => {
formMethods.setValue("length", Number(e.target.value));
}}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
/>
)}
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
@@ -343,7 +342,7 @@ export const EventSetupTab = (
saveLocation={saveLocation}
defaultValues={formMethods.getValues("locations")}
selection={
selectedLocation ? { value: selectedLocation.value, label: selectedLocation.label } : undefined
selectedLocation ? { value: selectedLocation.value, label: t(selectedLocation.label) } : undefined
}
setSelectedLocation={setSelectedLocation}
setEditingLocationType={setEditingLocationType}
+4 -49
View File
@@ -1,11 +1,12 @@
import { SchedulingType } from "@prisma/client/";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo } from "react";
import { Controller, useFormContext } from "react-hook-form";
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
import { Label, Select } from "@calcom/ui";
interface IMemberToValue {
id: number | null;
@@ -25,8 +26,7 @@ export const EventTeamTab = ({
eventType,
team,
teamMembers,
currentUserMembership,
}: Pick<EventTypeSetupInfered, "eventType" | "teamMembers" | "team" | "currentUserMembership">) => {
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => {
const formMethods = useFormContext<FormValues>();
const { t } = useLocale();
@@ -101,48 +101,3 @@ export const EventTeamTab = ({
</div>
);
};
const TeamProfileCard = ({
id,
name,
hasPermsToDelete,
email,
username,
}: {
id: number;
name: string | null;
email: string;
username: string | null;
hasPermsToDelete: boolean;
}) => {
const formMethods = useFormContext<FormValues>();
const avatar = `${WEBAPP_URL}/${username}/avatar.png`;
return (
<div className="flex items-center border border-gray-300 px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md">
<Avatar imageSrc={avatar} gravatarFallbackMd5="ajsdkwakhsd1231" size="md" alt="Team Image" />
<div className="flex flex-col pl-3">
<div className="">
<span className="pr-2 text-sm font-semibold leading-none text-black">{name}</span>
</div>
<p className="text-sm font-normal leading-normal text-gray-700">{email}</p>
</div>
{hasPermsToDelete && (
<div className="ml-auto">
<Button
type="button"
size="icon"
color="destructive"
StartIcon={Icon.FiTrash}
onClick={() => {
formMethods.setValue(
"users",
formMethods.getValues("users").filter((user) => user !== id.toString())
);
}}
/>
</div>
)}
</div>
);
};
@@ -0,0 +1,179 @@
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useState } from "react";
import { TbWebhook } from "react-icons/tb";
import { WebhookForm } from "@calcom/features/webhooks/components";
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Webhook } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui";
export const EventTeamWebhooksTab = ({
eventType,
team,
}: Pick<EventTypeSetupProps, "eventType" | "team">) => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: true,
});
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
const subscriberUrlReserved = (subscriberUrl: string, id?: string): boolean => {
return !!webhooks?.find(
(webhook) => webhook.subscriberUrl === subscriberUrl && (!id || webhook.id !== id)
);
};
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
},
onError(error) {
showToast(`${error.message}`, "error");
},
});
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
if (subscriberUrlReserved(values.subscriberUrl, values.id)) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
createWebhookMutation.mutate({
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
eventTypeId: eventType.id,
});
};
const NewWebhookButton = () => {
const { t } = useLocale();
return (
<Button
color="secondary"
data-testid="new_webhook"
StartIcon={Icon.FiPlus}
onClick={() => setCreateModalOpen(true)}>
{t("new_webhook")}
</Button>
);
};
return (
<div>
{team && webhooks && !isLoading && (
<>
<div>
<div>
<>
{webhooks.length ? (
<>
<div className="mt-4 mb-8 rounded-md border">
{webhooks.map((webhook, index) => {
return (
<WebhookListItem
key={webhook.id}
webhook={webhook}
lastItem={webhooks.length === index + 1}
onEditWebhook={() => {
setEditModalOpen(true);
setWebhookToEdit(webhook);
}}
/>
);
})}
</div>
<NewWebhookButton />
</>
) : (
<EmptyScreen
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
/>
)}
</>
</div>
</div>
{/* New webhook dialog */}
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
<WebhookForm
onSubmit={onCreateWebhook}
onCancel={() => setCreateModalOpen(false)}
apps={installedApps?.items.map((app) => app.slug)}
/>
</DialogContent>
</Dialog>
{/* Edit webhook dialog */}
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
<DialogContent title={t("edit_webhook")}>
<WebhookForm
webhook={webhookToEdit}
apps={installedApps?.items.map((app) => app.slug)}
onCancel={() => setEditModalOpen(false)}
onSubmit={(values: WebhookFormSubmitData) => {
if (subscriberUrlReserved(values.subscriberUrl, webhookToEdit?.id || "")) {
showToast(t("webhook_subscriber_url_reserved"), "error");
return;
}
if (values.changeSecret) {
values.secret = values.newSecret.length ? values.newSecret : null;
}
if (!values.payloadTemplate) {
values.payloadTemplate = null;
}
editWebhookMutation.mutate({
id: webhookToEdit?.id || "",
subscriberUrl: values.subscriberUrl,
eventTriggers: values.eventTriggers,
active: values.active,
payloadTemplate: values.payloadTemplate,
secret: values.secret,
eventTypeId: webhookToEdit?.eventTypeId || undefined,
});
}}
/>
</DialogContent>
</Dialog>
</>
)}
</div>
);
};
@@ -1,9 +1,9 @@
import { TFunction } from "next-i18next";
import { useRouter } from "next/router";
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
import { useMemo, useState } from "react";
import { Loader } from "react-feather";
import { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -37,9 +37,9 @@ import { EmbedButton, EmbedDialog } from "@components/Embed";
type Props = {
children: React.ReactNode;
eventType: EventTypeSetupInfered["eventType"];
currentUserMembership: EventTypeSetupInfered["currentUserMembership"];
team: EventTypeSetupInfered["team"];
eventType: EventTypeSetupProps["eventType"];
currentUserMembership: EventTypeSetupProps["currentUserMembership"];
team: EventTypeSetupProps["team"];
disableBorder?: boolean;
enabledAppsNumber: number;
installedAppsNumber: number;
@@ -150,13 +150,20 @@ function EventTypeSingleLayout({
enabledWorkflowsNumber,
});
// If there is a team put this navigation item within the tabs
if (team)
if (team) {
navigation.splice(2, 0, {
name: "scheduling_type",
href: `/event-types/${eventType.id}?tabName=team`,
icon: Icon.FiUsers,
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
});
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
});
}
return navigation;
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
@@ -294,7 +301,7 @@ function EventTypeSingleLayout({
</Button>
</div>
}>
<ClientSuspense fallback={<Loader />}>
<ClientSuspense fallback={<Icon.FiLoader />}>
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
<div className="hidden xl:block">
<VerticalTabs
@@ -57,7 +57,7 @@ export default function RecurringEventController({
{recurringEventState && (
<div data-testid="recurring-event-collapsible" className="text-sm">
<div className="flex items-center">
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
<input
type="number"
min="1"
@@ -89,7 +89,7 @@ export default function RecurringEventController({
/>
</div>
<div className="mt-4 flex items-center">
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
<input
type="number"
min="1"
@@ -105,7 +105,7 @@ export default function RecurringEventController({
setRecurringEventState(newVal);
}}
/>
<p className="mr-2 text-neutral-900">
<p className="text-neutral-900 ltr:mr-2 rtl:ml-2">
{t("events", {
count: recurringEventState.count,
})}
@@ -82,7 +82,7 @@ const DisableTwoFactorAuthModal = ({ onDisable, onCancel }: DisableTwoFactorAuth
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
@@ -177,7 +177,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
@@ -186,13 +186,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel }: EnableTwoFactorModalProps)
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
@@ -1,10 +1,10 @@
import { ShieldCheckIcon } from "@calcom/ui";
import { Icon } from "@calcom/ui";
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
return (
<div className="mb-4 sm:flex sm:items-start">
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
<ShieldCheckIcon className="h-6 w-6 text-white" />
<Icon.ShieldCheckIcon className="h-6 w-6 text-white" />
</div>
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
<h3 className="font-cal text-lg font-medium leading-6 text-gray-900" id="modal-title">
@@ -62,7 +62,7 @@ export function CalendarSwitch(props: {
}
);
return (
<div className="flex space-x-2 py-1">
<div className="flex space-x-2 py-1 rtl:space-x-reverse">
<Switch
key={props.externalId}
name="enabled"
@@ -90,7 +90,7 @@ const DisableTwoFactorAuthModal = ({
</div>
<div className="mt-5 sm:mt-4 sm:flex sm:flex-row-reverse">
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isDisabling}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isDisabling}>
{t("disable")}
</Button>
<Button color="secondary" onClick={onCancel}>
@@ -170,7 +170,7 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
<WithStep step={SetupStep.ConfirmPassword} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={handleSetup}
disabled={password.length === 0 || isSubmitting}>
{t("continue")}
@@ -179,13 +179,13 @@ const EnableTwoFactorModal = ({ onEnable, onCancel, open, onOpenChange }: Enable
<WithStep step={SetupStep.DisplayQrCode} current={step}>
<Button
type="submit"
className="ltr:ml-2 rtl:mr-2"
className="ltr:ml-2 ltr:mr-2 rtl:ml-2"
onClick={() => setStep(SetupStep.EnterTotpCode)}>
{t("continue")}
</Button>
</WithStep>
<WithStep step={SetupStep.EnterTotpCode} current={step}>
<Button type="submit" className="ltr:ml-2 rtl:mr-2" disabled={isSubmitting}>
<Button type="submit" className="ltr:ml-2 ltr:mr-2 rtl:ml-2" disabled={isSubmitting}>
{t("enable")}
</Button>
</WithStep>
+7 -2
View File
@@ -1,11 +1,12 @@
import classNames from "classnames";
import React from "react";
import { APP_NAME, LOGO } from "@calcom/lib/constants";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
import Credits from "../../../../packages/ui/v2/core/Credits";
interface Props {
title: string;
description: string;
@@ -23,6 +24,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
// eslint-disable-next-line @next/next/no-img-element
<img className="mb-auto h-4" src={LOGO} alt={`${APP_NAME} Logo`} />
)}
<div className={classNames(props.showLogo ? "text-center" : "", "sm:mx-auto sm:w-full sm:max-w-md")}>
{props.heading && <h2 className="font-cal text-center text-3xl text-neutral-900">{props.heading}</h2>}
</div>
@@ -35,7 +37,10 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
<div className="border-1 mx-2 rounded-md border-gray-200 bg-white px-4 py-10 sm:px-10">
{props.children}
</div>
<div className="mt-8 text-center text-sm text-neutral-600">{props.footerText}</div>
<div className="mt-8 text-center text-sm text-neutral-600">
{props.footerText}
<Credits />
</div>
</div>
</div>
);
+1 -1
View File
@@ -34,7 +34,7 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
);
return (
<li key={idx} className="-mr-2 inline-block">
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
{item.href ? (
<Link href={item.href}>
<a>{avatar}</a>
@@ -11,17 +11,7 @@ import { User } from "@calcom/prisma/client";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogHeader,
Icon,
Input,
Label,
StarIconSolid,
} from "@calcom/ui";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
export enum UsernameChangeStatusEnum {
UPGRADE = "UPGRADE",
@@ -240,7 +230,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
isInputUsernamePremium ? "text-orange-400" : "",
usernameIsAvailable ? "" : ""
)}>
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
{isInputUsernamePremium ? <Icon.StarIconSolid className="mt-[2px] w-6" /> : <></>}
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
</span>
</div>
@@ -7,7 +7,17 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { TRPCClientErrorLike } from "@calcom/trpc/client";
import { trpc } from "@calcom/trpc/react";
import { AppRouter } from "@calcom/trpc/server/routers/_app";
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Icon, Input, Label } from "@calcom/ui";
import {
Button,
Dialog,
DialogClose,
DialogContent,
DialogHeader,
Icon,
Input,
Label,
TextField,
} from "@calcom/ui";
interface ICustomUsernameProps {
currentUsername: string | undefined;
@@ -77,10 +87,9 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
const ActionButtons = () => {
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
<div className="flex flex-row">
<div className="rlt:space-x-reverse mt-px flex flex-row space-x-2 ltr:ml-2 ltr:mr-2 rtl:ml-2 rtl:space-x-reverse">
<Button
type="button"
className="mx-2"
onClick={() => setOpenDialogSaveUsername(true)}
data-testid="update-username-btn">
{t("update")}
@@ -88,7 +97,6 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
<Button
type="button"
color="minimal"
className="mx-2"
onClick={() => {
if (currentUsername) {
setInputUsernameValue(currentUsername);
@@ -110,26 +118,20 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
return (
<div>
<div>
<Label htmlFor="username">{t("username")}</Label>
</div>
<div className="mt-2 flex rounded-md">
<span
className={classNames(
"inline-flex items-center rounded-l-md border border-r-0 border-gray-300 bg-gray-50 px-3 text-sm text-gray-500"
)}>
{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/
</span>
<div className="relative w-full">
<Input
<TextField
ref={usernameRef}
name="username"
value={inputUsernameValue}
addOnLeading={
<>{process.env.NEXT_PUBLIC_WEBSITE_URL.replace("https://", "").replace("http://", "")}/</>
}
autoComplete="none"
autoCapitalize="none"
autoCorrect="none"
className={classNames(
"mb-0 mt-0 h-6 rounded-md rounded-l-none",
"mb-0 mt-0 h-6 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
markAsError
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
: ""
@@ -141,18 +143,18 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
data-testid="username-input"
/>
{currentUsername !== inputUsernameValue && (
<div className="absolute right-[2px] top-0 flex flex-row">
<div className="absolute right-[2px] top-6 flex flex-row">
<span className={classNames("mx-2 py-2")}>
{usernameIsAvailable ? <Icon.FiCheck className="mt-[2px] w-6" /> : <></>}
{usernameIsAvailable ? <Icon.FiCheck className="w-6" /> : <></>}
</span>
</div>
)}
</div>
<div className="hidden md:inline">
<div className="mt-5 hidden md:inline">
<ActionButtons />
</div>
</div>
{markAsError && <p className="mt-1 text-xs text-red-500">Username is already taken</p>}
{markAsError && <p className="mt-1 text-xs text-red-500">{t("username_already_taken")}</p>}
{usernameIsAvailable && currentUsername !== inputUsernameValue && (
<div className="mt-2 flex justify-end md:hidden">
+1 -1
View File
@@ -109,7 +109,7 @@ function Select<
IndicatorSeparator: () => null,
Input: InputComponent,
}}
className={classNames("text-sm", className)}
className={classNames("border-0 text-sm", className)}
{...props}
/>
);
+1 -1
View File
@@ -1,7 +1,6 @@
import parser from "accept-language-parser";
import { IncomingMessage } from "http";
import prisma from "@calcom/prisma";
import { Maybe } from "@calcom/trpc/server";
import { getSession } from "@lib/auth";
@@ -17,6 +16,7 @@ export function getLocaleFromHeaders(req: IncomingMessage): string {
}
export const getOrSetUserLocaleFromHeaders = async (req: IncomingMessage): Promise<string> => {
const prisma = (await import("@calcom/prisma")).default;
const session = await getSession({ req });
const preferredLocale = getLocaleFromHeaders(req);
+5
View File
@@ -227,6 +227,11 @@ const nextConfig = {
destination: "/404",
permanent: false,
},
{
source: "/booking/direct/:action/:email/:bookingUid/:oldToken",
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
permanent: true,
},
];
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
+3 -3
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.3.7",
"version": "2.4.3",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -61,7 +61,7 @@
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
"@vercel/edge-functions-ui": "^0.2.1",
"@vercel/og": "^0.0.19",
"@vercel/og": "^0.0.21",
"accept-language-parser": "^1.5.0",
"async": "^3.2.4",
"bcryptjs": "^2.4.3",
@@ -121,7 +121,7 @@
"tailwindcss-radix": "^2.6.0",
"uuid": "^8.3.2",
"web3": "^1.7.5",
"zod": "^3.19.1"
"zod": "^3.20.2"
},
"devDependencies": {
"@babel/core": "^7.19.6",
+29 -13
View File
@@ -1,32 +1,48 @@
import Head from "next/head";
import { useRouter } from "next/router";
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui";
import { Button, Icon, showToast } from "@calcom/ui";
export default function Error500() {
const { t } = useLocale();
const router = useRouter();
return (
<div className="flex h-screen">
<div className="flex h-screen bg-gray-100">
<Head>
<title>Something unexpected occurred | {APP_NAME}</title>
<link rel="icon" href="/favicon.ico" />
</Head>
<div className="m-auto text-center">
<h1 className="font-cal text-[250px] text-gray-900">
5
{
// eslint-disable-next-line @next/next/no-img-element
<img src="/error.svg" className="-mt-10 inline w-60" alt="0" />
}
0
</h1>
<h2 className="mb-2 -mt-16 text-3xl text-gray-600">It&apos;s not you, it&apos;s us.</h2>
<p className="mb-4 max-w-2xl text-gray-500">
<div className="rtl: m-auto rounded-md bg-white p-10 text-right ltr:text-left">
<h1 className="font-cal text-6xl text-black">500</h1>
<h2 className="mt-6 text-2xl font-medium text-black">It&apos;s not you, it&apos;s us.</h2>
<p className="mt-4 mb-6 max-w-2xl text-sm text-gray-600">
Something went wrong on our end. Get in touch with our support team, and we’ll get it fixed right
away for you.
</p>
{router.query.error && (
<div className="mb-8 flex flex-col">
<p className="mb-4 max-w-2xl text-sm text-gray-600">
Please provide the following text when contacting support to better help you:
</p>
<pre className="w-full max-w-2xl whitespace-normal break-words rounded-md bg-gray-200 p-4 text-gray-900">
{router.query.error}
<br />
<Button
color="secondary"
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
StartIcon={Icon.FiCopy}
onClick={() => {
navigator.clipboard.writeText(router.query.error as string);
showToast("Link copied!", "success");
}}>
{t("copy")}
</Button>
</pre>
</div>
)}
<Button href={`${WEBSITE_URL}/support`}>{t("contact_support")}</Button>
<Button color="secondary" href="javascript:history.back()" className="ml-2">
Go back
+5 -4
View File
@@ -12,6 +12,7 @@ import {
useEmbedStyles,
useIsEmbed,
} from "@calcom/embed-core/embed-iframe";
import { EventTypeDescriptionLazy as EventTypeDescription } from "@calcom/features/eventtypes/components";
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
import CustomBranding from "@calcom/lib/CustomBranding";
import defaultEvents, {
@@ -26,7 +27,7 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
import prisma from "@calcom/prisma";
import { baseEventTypeSelect } from "@calcom/prisma/selects";
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { BadgeCheckIcon, EventTypeDescriptionLazy as EventTypeDescription, Icon } from "@calcom/ui";
import { Icon } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
@@ -58,7 +59,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
{eventTypes.map((type, index) => (
<li
key={index}
className="dark:bg-darkgray-100 dark:border-darkgray-200 group relative rounded-sm border border-neutral-200 bg-white dark:hover:border-neutral-600">
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
@@ -68,7 +69,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
</div>
<div className="mt-1 self-center">
<AvatarGroup
border="border-2 border-white"
border="border-2 border-white dark:border-darkgray-100"
truncateAfter={4}
className="flex flex-shrink-0"
size={10}
@@ -138,7 +139,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
<h1 className="font-cal mb-1 text-3xl text-neutral-900 dark:text-white">
{nameOrUsername}
{user.verified && (
<BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
)}
</h1>
<p className="dark:text-darkgray-600 text-s text-neutral-500">{user.bio}</p>
+4 -3
View File
@@ -4,7 +4,7 @@ import { JSONObject } from "superjson/dist/types";
import { z } from "zod";
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defaultEvents";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
@@ -157,9 +157,10 @@ async function getUserPageProps(context: GetStaticPropsContext) {
// Check if the user you are logging into has any active teams
const hasActiveTeam =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
return {
-2
View File
@@ -19,7 +19,6 @@ import { seoConfig } from "@lib/config/next-seo.config";
import I18nLanguageHandler from "@components/I18nLanguageHandler";
import "../styles/fonts.css";
import "../styles/globals.css";
function MyApp(props: AppProps) {
@@ -32,7 +31,6 @@ function MyApp(props: AppProps) {
}
// Use the layout defined at the page level, if available
const getLayout = Component.getLayout ?? ((page) => page);
return (
<AppProviders {...props}>
<DefaultSeo {...seoConfig.defaultNextSeo} />
+3 -1
View File
@@ -1,6 +1,8 @@
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
import Script from "next/script";
import { getDirFromLang } from "@calcom/lib/i18n";
type Props = Record<string, unknown> & DocumentProps;
function toRunBeforeReactOnClient() {
@@ -47,7 +49,7 @@ class MyDocument extends Document<Props> {
render() {
const { locale } = this.props.__NEXT_DATA__;
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
const dir = getDirFromLang(locale);
return (
<Html lang={locale} dir={dir}>
<Head>
+5 -4
View File
@@ -14,7 +14,7 @@ import checkLicense from "@calcom/features/ee/common/server/checkLicense";
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultCookies } from "@calcom/lib/default-cookies";
import rateLimit from "@calcom/lib/rateLimit";
@@ -125,9 +125,10 @@ const providers: Provider[] = [
// Check if the user you are logging into has any active teams
const hasActiveTeams =
user.teams.filter((m) => {
if (!IS_TEAM_BILLING_ENABLED) return true;
const metadata = teamMetadataSchema.safeParse(m.team.metadata);
if (metadata.success && metadata.data?.subscriptionId) return false;
return true;
if (metadata.success && metadata.data?.subscriptionId) return true;
return false;
}).length > 0;
// authentication success- but does it meet the minimum password requirements?
@@ -137,7 +138,7 @@ const providers: Provider[] = [
username: user.username,
email: user.email,
name: user.name,
role: "USER",
role: "INACTIVE_ADMIN",
belongsToActiveTeam: hasActiveTeams,
};
}
@@ -0,0 +1,46 @@
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { fetcher } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
import { getSession } from "@calcom/lib/auth";
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
const getAccessLinkSchema = z.union([
z.object({
download_link: z.string(),
expires: z.number(),
}),
z.object({}),
]);
const requestQuery = z.object({
recordingId: z.string(),
});
async function handler(
req: NextApiRequest,
res: NextApiResponse<z.infer<typeof getAccessLinkSchema> | void>
) {
const { recordingId } = requestQuery.parse(req.query);
const session = await getSession({ req });
// Check if user belong to active team
if (!session?.user?.belongsToActiveTeam) {
return res.status(403);
}
try {
const response = await fetcher(`/recordings/${recordingId}/access-link`).then(getAccessLinkSchema.parse);
if ("download_link" in response && response.download_link) {
return res.status(200).json(response);
}
return res.status(400);
} catch (err) {
res.status(500);
}
}
export default defaultHandler({
GET: Promise.resolve({ default: defaultResponder(handler) }),
});
+4 -23
View File
@@ -13,10 +13,6 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
title: "30min between Pro Example and pro@example.com",
description: null,
additionalNotes: "asdasdas",
customInputs: {
"Custom input 01": "sadasdasdsadasd",
"Custom input 02": "asdasdasd",
},
startTime: "2022-06-03T09:00:00-06:00",
endTime: "2022-06-03T09:30:00-06:00",
organizer: {
@@ -32,26 +28,12 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
{
email: "pro@example.com",
name: "pro@example.com",
timeZone: "America/Chihuahua",
language,
},
],
location: "Zoom video",
destinationCalendar: null,
hideCalendarNotes: false,
uid: "bwPWLpjYrx4rZf6MCZdKgE",
uid: "xxyPr4cg2xx4XoS2KeMEQy",
metadata: {},
cancellationReason: "It got late",
paymentInfo: { id: "pi_12312", link: "https://cal.com", reason: "no reason" },
recurringEvent: null,
};
@@ -60,10 +42,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
res.setHeader("Content-Type", "text/html");
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
res.write(
renderEmail("DisabledAppEmail", {
appName: "Stripe",
appType: ["payment"],
t,
renderEmail("OrganizerRequestEmail", {
attendee: evt.attendees[0],
calEvent: evt,
})
);
res.end();
+71
View File
@@ -0,0 +1,71 @@
import { UserPermissionRole } from "@prisma/client";
import type { NextApiRequest, NextApiResponse } from "next";
import { z } from "zod";
import { symmetricDecrypt } from "@calcom/lib/crypto";
import { defaultResponder } from "@calcom/lib/server";
import prisma from "@calcom/prisma";
import { TRPCError } from "@calcom/trpc/server";
import { createContext } from "@calcom/trpc/server/createContext";
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
enum DirectAction {
ACCEPT = "accept",
REJECT = "reject",
}
const querySchema = z.object({
action: z.nativeEnum(DirectAction),
token: z.string(),
reason: z.string().optional(),
});
const decryptedSchema = z.object({
bookingUid: z.string(),
userId: z.number().int(),
});
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, reason } = querySchema.parse(req.query);
const { bookingUid, userId } = decryptedSchema.parse(
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
);
/** We shape the session as required by tRPC router */
async function sessionGetter() {
return {
user: {
id: userId,
username: "" /* Not used in this context */,
role: UserPermissionRole.USER,
},
hasValidLicense: true,
expires: "" /* Not used in this context */,
};
}
const booking = await prisma.booking.findUniqueOrThrow({
where: { uid: bookingUid },
});
try {
/** @see https://trpc.io/docs/server-side-calls */
const ctx = await createContext({ req, res }, sessionGetter);
const caller = viewerRouter.createCaller(ctx);
await caller.bookings.confirm({
bookingId: booking.id,
recurringEventId: booking.recurringEventId || undefined,
confirmed: action === DirectAction.ACCEPT,
reason,
});
} catch (e) {
let message = "Error confirming booking";
if (e instanceof TRPCError) message = e.message;
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
return;
}
res.redirect(`/booking/${bookingUid}`);
}
export default defaultResponder(handler);
+16 -4
View File
@@ -53,6 +53,8 @@ export default async function handler(req: NextApiRequest) {
interFontMedium,
]);
const ogConfig = {
width: 1200,
height: 630,
fonts: [
{ name: "inter", data: interFontData, weight: 400 },
{ name: "inter", data: interFontMediumData, weight: 500 },
@@ -71,7 +73,7 @@ export default async function handler(req: NextApiRequest) {
meetingImage: searchParams.get("meetingImage"),
imageType,
});
return new ImageResponse(
const img = new ImageResponse(
(
<Meeting
title={title}
@@ -80,7 +82,9 @@ export default async function handler(req: NextApiRequest) {
/>
),
ogConfig
);
) as { body: Buffer };
return new Response(img.body, { status: 200 });
}
case "app": {
const { name, description, slug } = appSchema.parse({
@@ -89,7 +93,11 @@ export default async function handler(req: NextApiRequest) {
slug: searchParams.get("slug"),
imageType,
});
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
const img = new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig) as {
body: Buffer;
};
return new Response(img.body, { status: 200 });
}
case "generic": {
@@ -99,7 +107,11 @@ export default async function handler(req: NextApiRequest) {
imageType,
});
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
const img = new ImageResponse(<Generic title={title} description={description} />, ogConfig) as {
body: Buffer;
};
return new Response(img.body, { status: 200 });
}
default:
@@ -47,13 +47,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
}
export const getStaticPaths = async () => {
const appStore = await getAppRegistry();
const paths = appStore.reduce((categories, app) => {
if (!categories.includes(app.category)) {
categories.push(app.category);
}
return categories;
}, [] as string[]);
const paths = Object.keys(AppCategories);
return {
paths: paths.map((category) => ({ params: { category } })),
+3 -1
View File
@@ -44,7 +44,9 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
export const getStaticProps = async () => {
const appStore = await getAppRegistry();
const categories = appStore.reduce((c, app) => {
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
for (const category of app.categories) {
c[category] = c[category] ? c[category] + 1 : 1;
}
return c;
}, {} as Record<string, number>);
+9 -7
View File
@@ -1,4 +1,4 @@
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
import { GetServerSidePropsContext } from "next";
import { ChangeEventHandler, useState } from "react";
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
@@ -6,6 +6,7 @@ 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 { inferSSRProps } from "@calcom/types/inferSSRProps";
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import { ssgInit } from "@server/lib/ssg";
@@ -30,10 +31,7 @@ function AppsSearch({
);
}
export default function Apps({
categories,
appStore,
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const [searchText, setSearchText] = useState<string | undefined>(undefined);
@@ -52,12 +50,16 @@ export default function Apps({
<TrendingAppsSlider items={appStore} />
</>
)}
<AllApps apps={appStore} searchText={searchText} />
<AllApps
apps={appStore}
searchText={searchText}
categories={categories.map((category) => category.name)}
/>
</AppsLayout>
);
}
export const getServerSideProps: GetServerSideProps = async (context) => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssg = await ssgInit(context);
const session = await getSession(context);
+1 -1
View File
@@ -17,7 +17,7 @@ import {
InstalledAppsLayout,
List,
ShellSubHeading,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
+3 -2
View File
@@ -7,12 +7,13 @@ import { useState } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { SAMLLogin } from "@calcom/features/auth/SAMLLogin";
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import prisma from "@calcom/prisma";
import { Alert, Button, EmailField, Icon, PasswordField, SAMLLogin } from "@calcom/ui";
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
import { ErrorCode, getSession } from "@lib/auth";
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
@@ -131,7 +132,7 @@ export default function Login({
{...register("email")}
/>
<div className="relative">
<div className="absolute right-0 -top-[6px] z-10">
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
<Link href="/auth/forgot-password">
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
{t("forgot")}
+25 -3
View File
@@ -1,6 +1,9 @@
import { UserPermissionRole } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { getSession } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -24,7 +27,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{
title: t("enable_apps"),
description: t("enable_apps_description"),
content: <AdminAppsList baseURL="/auth/setup" />,
content: <AdminAppsList baseURL="/auth/setup?step=2" />,
isLoading: false,
},
];
@@ -32,14 +35,33 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
return (
<>
<main className="flex items-center bg-gray-100 print:h-full">
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
<WizardForm
href="/auth/setup"
steps={steps}
nextLabel={t("next_step_text")}
finishLabel={t("finish")}
prevLabel={t("prev_step")}
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
/>
</main>
</>
);
}
export const getServerSideProps = async () => {
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const userCount = await prisma.user.count();
const { req } = context;
const session = await getSession({ req });
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
return {
redirect: {
destination: `/404`,
permanent: false,
},
};
}
return {
props: {
userCount,
@@ -97,7 +97,7 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
<TextField
addOnLeading={
!longWebsiteUrl && (
<span className="items-centerpx-3 inline-flex rounded-none text-sm text-gray-500">
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500">
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
</span>
)
+19 -7
View File
@@ -1,18 +1,30 @@
import { useRouter } from "next/router";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Icon } from "@calcom/ui";
const StepDone = () => {
const router = useRouter();
const { t } = useLocale();
return (
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
<form
id="wizard-step-1"
name="wizard-step-1"
className="space-y-4"
onSubmit={(e) => {
e.preventDefault();
router.replace(`/auth/setup?step=2&category=calendar`);
}}>
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
<div className="max-w-[420px] text-center">
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
</div>
</div>
</form>
);
};
+129 -102
View File
@@ -1,15 +1,16 @@
import { GetStaticPaths, GetStaticProps } from "next";
import { useEffect } from "react";
import { Controller, useForm } from "react-hook-form";
import { GetServerSidePropsContext } from "next";
import { Controller, useFieldArray, useForm } from "react-hook-form";
import { z } from "zod";
import { DateOverrideInputDialog, DateOverrideList } from "@calcom/features/schedules";
import Schedule from "@calcom/features/schedules/components/Schedule";
import { availabilityAsString } from "@calcom/lib/availability";
import { yyyymmdd } from "@calcom/lib/date-fns";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { stringOrNumber } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
import type { Schedule as ScheduleType } from "@calcom/types/schedule";
import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/types/schedule";
import {
Button,
Form,
@@ -21,6 +22,7 @@ import {
SkeletonText,
Switch,
TimezoneSelect,
Tooltip,
VerticalDivider,
} from "@calcom/ui";
@@ -29,7 +31,7 @@ import { HttpError } from "@lib/core/http/error";
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
import EditableHeading from "@components/ui/EditableHeading";
import { ssgInit } from "@server/lib/ssg";
import { ssrInit } from "@server/lib/ssr";
const querySchema = z.object({
schedule: stringOrNumber,
@@ -38,31 +40,59 @@ const querySchema = z.object({
type AvailabilityFormValues = {
name: string;
schedule: ScheduleType;
dateOverrides: { ranges: TimeRange[] }[];
timeZone: string;
isDefault: boolean;
};
const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
const { remove, append, update, fields } = useFieldArray<AvailabilityFormValues, "dateOverrides">({
name: "dateOverrides",
});
const { t } = useLocale();
return (
<div className="px-4 py-5 sm:p-6">
<h3 className="font-medium leading-6 text-gray-900">
{t("date_overrides")}{" "}
<Tooltip content={t("date_overrides_info")}>
<span className="inline-block">
<Icon.FiInfo />
</span>
</Tooltip>
</h3>
<p className="mb-4 text-sm text-neutral-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
<div className="mt-1 space-y-2">
<DateOverrideList
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
remove={remove}
update={update}
items={fields}
workingHours={workingHours}
/>
<DateOverrideInputDialog
workingHours={workingHours}
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
onChange={(ranges) => append({ ranges })}
Trigger={
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
Add an override
</Button>
}
/>
</div>
</div>
);
};
export default function Availability({ schedule }: { schedule: number }) {
const { t, i18n } = useLocale();
const utils = trpc.useContext();
const me = useMeQuery();
const { timeFormat } = me.data || { timeFormat: null };
const { data, isLoading } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>();
const { control, reset } = form;
useEffect(() => {
if (!isLoading && data) {
reset({
name: data?.schedule?.name,
schedule: data.availability,
timeZone: data.timeZone,
isDefault: data.isDefault,
});
}
}, [data, isLoading, reset]);
const { data: defaultValues } = trpc.viewer.availability.defaultValues.useQuery({ scheduleId: schedule });
const form = useForm<AvailabilityFormValues>({ defaultValues });
const { control } = form;
const updateMutation = trpc.viewer.availability.schedule.update.useMutation({
onSuccess: async ({ prevDefaultId, currentDefaultId, ...data }) => {
if (prevDefaultId && currentDefaultId) {
@@ -73,7 +103,7 @@ export default function Availability({ schedule }: { schedule: number }) {
utils.viewer.availability.schedule.get.refetch({ scheduleId: prevDefaultId });
}
}
utils.viewer.availability.schedule.get.setData({ scheduleId: data.schedule.id }, data);
utils.viewer.availability.schedule.get.invalidate({ scheduleId: data.schedule.id });
utils.viewer.availability.list.invalidate();
showToast(
t("availability_updated_successfully", {
@@ -98,17 +128,21 @@ export default function Availability({ schedule }: { schedule: number }) {
<Controller
control={form.control}
name="name"
render={({ field }) => <EditableHeading isReady={!isLoading} {...field} />}
render={({ field }) => (
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
)}
/>
}
subtitle={
data ? (
data.schedule.availability.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
data.schedule.availability
.filter((availability) => !!availability.days.length)
.map((availability) => (
<span key={availability.id}>
{availabilityAsString(availability, { locale: i18n.language, hour12: timeFormat === 12 })}
<br />
</span>
))
) : (
<SkeletonText className="h-4 w-48" />
)
@@ -140,95 +174,88 @@ export default function Availability({ schedule }: { schedule: number }) {
</Button>
</div>
}>
<div className="flex items-baseline sm:mt-0">
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async (values) => {
updateMutation.mutate({
scheduleId: schedule,
...values,
});
}}
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1">
<div className="rounded-md border-gray-200 bg-white py-5 pr-4 sm:border sm:p-6">
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
name="schedule"
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
me.data?.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
/>
)}
</div>
<div className="w-full">
<Form
form={form}
id="availability-form"
handleSubmit={async ({ dateOverrides, ...values }) => {
updateMutation.mutate({
scheduleId: schedule,
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
...values,
});
}}
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
<div className=" py-5 sm:p-6">
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
{t("change_start_end")}
</h3>
{typeof me.data?.weekStart === "string" && (
<Schedule
control={control}
name="schedule"
weekStart={
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
me.data?.weekStart
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
}
/>
)}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="w-72" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
</div>
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
<div>
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
{t("timezone")}
</label>
<Controller
name="timeZone"
render={({ field: { onChange, value } }) =>
value ? (
<TimezoneSelect
value={value}
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
onChange={(timezone) => onChange(timezone.value)}
/>
) : (
<SelectSkeletonLoader className="w-72" />
)
}
/>
</div>
<hr className="my-8" />
<div className="rounded-md">
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
<div className="mt-3 flex">
<Button href="/availability/troubleshoot" color="secondary">
{t("launch_troubleshooter")}
</Button>
</div>
</div>
</div>
</Form>
</div>
</div>
</Form>
</div>
</Shell>
);
}
export const getStaticProps: GetStaticProps = async (ctx) => {
export const getServerSideProps = async (ctx: GetServerSidePropsContext) => {
const params = querySchema.safeParse(ctx.params);
const ssg = await ssgInit(ctx);
const ssr = await ssrInit(ctx);
if (!params.success) return { notFound: true };
const scheduleId = params.data.schedule;
await ssr.viewer.availability.schedule.get.fetch({ scheduleId });
await ssr.viewer.availability.defaultValues.fetch({ scheduleId });
return {
props: {
schedule: params.data.schedule,
trpcState: ssg.dehydrate(),
schedule: scheduleId,
trpcState: ssr.dehydrate(),
},
revalidate: 10, // seconds
};
};
export const getStaticPaths: GetStaticPaths = () => {
return {
paths: [],
fallback: "blocking",
};
};
+58 -37
View File
@@ -1,10 +1,10 @@
import { useState } from "react";
import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Shell, SkeletonText } from "@calcom/ui";
import useRouterQuery from "@lib/hooks/useRouterQuery";
type User = RouterOutputs["viewer"]["me"];
export interface IBusySlot {
@@ -16,7 +16,9 @@ export interface IBusySlot {
const AvailabilityView = ({ user }: { user: User }) => {
const { t } = useLocale();
const [selectedDate, setSelectedDate] = useState(dayjs());
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
const selectedDate = dayjs(date);
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
{
@@ -30,6 +32,17 @@ const AvailabilityView = ({ user }: { user: User }) => {
}
);
const overrides =
data?.dateOverrides.reduce((acc, override) => {
if (
formattedSelectedDate !== dayjs(override.start).format("YYYY-MM-DD") &&
formattedSelectedDate !== dayjs(override.end).format("YYYY-MM-DD")
)
return acc;
acc.push({ ...override, source: "Date override" });
return acc;
}, [] as IBusySlot[]) || [];
return (
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
<div className="px-4 py-5 sm:p-6">
@@ -37,9 +50,9 @@ const AvailabilityView = ({ user }: { user: User }) => {
<input
type="date"
className="inline h-8 border-none p-0"
defaultValue={selectedDate.format("YYYY-MM-DD")}
defaultValue={formattedSelectedDate}
onChange={(e) => {
if (e.target.value) setSelectedDate(dayjs(e.target.value));
if (e.target.value) setSelectedDate(e.target.value);
}}
/>
<small className="block text-neutral-400">{t("hover_over_bold_times_tip")}</small>
@@ -49,39 +62,47 @@ const AvailabilityView = ({ user }: { user: User }) => {
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
</div>
</div>
{isLoading ? (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
) : data && data.busy.length > 0 ? (
data.busy
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
{(() => {
if (isLoading)
return (
<>
<SkeletonText className="block h-16 w-full" />
<SkeletonText className="block h-16 w-full" />
</>
);
if (data && (data.busy.length > 0 || overrides.length > 0))
return [...data.busy, ...overrides]
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
.map((slot: IBusySlot) => (
<div
key={dayjs(slot.start).format("HH:mm")}
className="overflow-hidden rounded-md bg-neutral-100"
data-testid="troubleshooter-busy-time">
<div className="px-4 py-5 text-black sm:p-6">
{t("calendar_shows_busy_between")}{" "}
<span
className="font-medium text-neutral-800"
title={dayjs(slot.start).format("HH:mm")}>
{dayjs(slot.start).format("HH:mm")}
</span>{" "}
{t("and")}{" "}
<span className="font-medium text-neutral-800" title={dayjs(slot.end).format("HH:mm")}>
{dayjs(slot.end).format("HH:mm")}
</span>{" "}
{t("on")} {dayjs(slot.start).format("D")}{" "}
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
{slot.title && ` - (${slot.title})`}
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
</div>
</div>
</div>
))
) : (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
)}
));
return (
<div className="overflow-hidden rounded-md bg-neutral-100">
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
</div>
);
})()}
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
+38 -13
View File
@@ -168,17 +168,18 @@ const querySchema = z.object({
changes: stringToBoolean,
reschedule: stringToBoolean,
isSuccessBookingPage: z.string().optional(),
formerTime: z.string().optional(),
});
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const {
allRemainingBookings,
isSuccessBookingPage,
cancel: isCancellationMode,
changes,
formerTime,
} = querySchema.parse(router.query);
if ((isCancellationMode || changes) && typeof window !== "undefined") {
@@ -425,9 +426,11 @@ export default function Success(props: SuccessProps) {
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p>
</div>
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
{isCancelled && cancellationReason && (
{(isCancelled || reschedule) && cancellationReason && (
<>
<div className="font-medium">{t("reason")}</div>
<div className="font-medium">
{isCancelled ? t("reason") : t("reschedule_reason_success_page")}
</div>
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
</>
)}
@@ -435,6 +438,19 @@ export default function Success(props: SuccessProps) {
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
<div className="font-medium">{t("when")}</div>
<div className="col-span-2 mb-6 last:mb-0">
{reschedule && !!formerTime && (
<p className="line-through">
<RecurringBookings
eventType={props.eventType}
duration={calculatedDuration}
recurringBookings={props.recurringBookings}
allRemainingBookings={allRemainingBookings}
date={dayjs(formerTime)}
is24h={is24h}
isCancelled={isCancelled}
/>
</p>
)}
<RecurringBookings
eventType={props.eventType}
duration={calculatedDuration}
@@ -619,7 +635,7 @@ export default function Success(props: SuccessProps) {
encodeURIComponent(new RRule(props.eventType.recurringEvent).toString())
: "")
}>
<a className="mr-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white">
<a className="h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 ltr:mr-2 rtl:ml-2 dark:border-neutral-700 dark:text-white">
<svg
className="-mt-1.5 inline-block h-4 w-4"
fill="currentColor"
@@ -790,10 +806,10 @@ 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" : "")}>
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -808,10 +824,10 @@ export function RecurringBookings({
{eventType.recurringEvent?.count &&
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
<br />
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
))}
@@ -824,10 +840,10 @@ export function RecurringBookings({
return (
<div className={classNames(isCancelled ? "line-through" : "")}>
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
{dayjs.tz(date, timeZone()).format("dddd, DD MMMM YYYY")}
<br />
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
{formatTime(date, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
<span className="text-bookinglight">({timeZone()})</span>
</div>
);
@@ -1034,6 +1050,15 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
customInputs: customInputSchema.array().parse(eventTypeRaw.customInputs),
};
if (eventType.metadata?.disableSuccessPage && eventType.successRedirectUrl) {
return {
redirect: {
destination: eventType.successRedirectUrl,
permanent: false,
},
};
}
const profile = {
name: eventType.team?.name || eventType.users[0]?.name || null,
email: eventType.team ? null : eventType.users[0].email || null,
+15 -6
View File
@@ -5,9 +5,11 @@ import { Fragment } from "react";
import { z } from "zod";
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
import BookingLayout from "@calcom/features/bookings/layout/BookingLayout";
import { filterQuerySchema, useFilterQuery } from "@calcom/features/bookings/lib/useFilterQuery";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
import { Alert, BookingLayout, Button, EmptyScreen, Icon } from "@calcom/ui";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui";
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
@@ -16,7 +18,7 @@ import SkeletonLoader from "@components/booking/SkeletonLoader";
import { ssgInit } from "@server/lib/ssg";
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
type BookingListingStatus = z.infer<typeof filterQuerySchema>["status"];
type BookingOutput = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][0];
type RecurringInfo = {
@@ -41,12 +43,19 @@ const querySchema = z.object({
});
export default function Bookings() {
const { data: filterQuery } = useFilterQuery();
const router = useRouter();
const { status } = router.isReady ? querySchema.parse(router.query) : { status: "upcoming" as const };
const { t } = useLocale();
const query = trpc.viewer.bookings.get.useInfiniteQuery(
{ status, limit: 10 },
{
limit: 10,
filters: {
...filterQuery,
status: filterQuery.status ?? status,
},
},
{
// first render has status `undefined`
enabled: router.isReady,
@@ -128,7 +137,7 @@ export default function Bookings() {
</div>
)}
<div className="pt-2 xl:pt-0">
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p>
{/* <p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p> */}
<div className="overflow-hidden rounded-md border border-gray-200">
<table className="w-full max-w-full table-fixed">
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
@@ -165,7 +174,7 @@ export default function Bookings() {
</>
)}
{query.status === "success" && isEmpty && (
<div className="flex items-center justify-center pt-2 xl:mx-6 xl:pt-0">
<div className="flex items-center justify-center pt-2 xl:pt-0">
<EmptyScreen
Icon={Icon.FiCalendar}
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
+72 -254
View File
@@ -1,26 +1,24 @@
/* eslint-disable @typescript-eslint/no-empty-function */
import { useAutoAnimate } from "@formkit/auto-animate/react";
import { PeriodType, Prisma, SchedulingType } from "@prisma/client";
import { zodResolver } from "@hookform/resolvers/zod";
import { PeriodType, SchedulingType } from "@prisma/client";
import { GetServerSidePropsContext } from "next";
import { useRouter } from "next/router";
import { useState } from "react";
import { useForm } from "react-hook-form";
import { z } from "zod";
import { StripeData } from "@calcom/app-store/stripepayment/lib/server";
import { getEventTypeAppData, getLocationOptions } from "@calcom/app-store/utils";
import { EventLocationType, LocationObject } from "@calcom/core/location";
import { parseBookingLimit, parseRecurringEvent, validateBookingLimitOrder } from "@calcom/lib";
import getEnabledApps from "@calcom/lib/apps/getEnabledApps";
import { EventLocationType } from "@calcom/core/location";
import { validateBookingLimitOrder } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
import findDurationType from "@calcom/lib/findDurationType";
import getStripeAppData from "@calcom/lib/getStripeAppData";
import getEventTypeById from "@calcom/lib/getEventTypeById";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { HttpError } from "@calcom/lib/http-error";
import prisma from "@calcom/prisma";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { trpc } from "@calcom/trpc/react";
import { trpc, RouterOutputs } from "@calcom/trpc/react";
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
import { Form, showToast } from "@calcom/ui";
@@ -36,10 +34,10 @@ import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
import { EventTeamTab } from "@components/eventtype/EventTeamTab";
import { EventTeamWebhooksTab } from "@components/eventtype/EventTeamWebhooksTab";
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
import { getTranslation } from "@server/lib/i18n";
import { ssrInit } from "@server/lib/ssr";
export type FormValues = {
@@ -93,32 +91,39 @@ export type CustomInputParsed = typeof customInputSchema._output;
const querySchema = z.object({
tabName: z
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
.enum([
"setup",
"availability",
"apps",
"limits",
"recurring",
"team",
"advanced",
"workflows",
"webhooks",
])
.optional()
.default("setup"),
});
export type EventTypeSetupInfered = inferSSRProps<typeof getServerSideProps>;
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
const EventTypePage = (props: EventTypeSetupProps) => {
const { t } = useLocale();
const utils = trpc.useContext();
const router = useRouter();
const { tabName } = querySchema.parse(router.query);
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
extendsFeature: "EventType",
});
const { eventType: dbEventType, locationOptions, team, teamMembers } = props;
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
const [eventType, setEventType] = useState(dbEventType);
const router = useRouter();
const { tabName } = querySchema.parse(router.query);
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async ({ eventType: newEventType }) => {
setEventType({ ...eventType, slug: newEventType.slug });
onSuccess: async () => {
showToast(
t("event_type_updated_successfully", {
eventTypeTitle: eventType.title,
@@ -126,6 +131,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
"success"
);
},
async onSettled() {
await utils.viewer.eventTypes.get.invalidate();
},
onError: (err) => {
let message = "";
if (err instanceof HttpError) {
@@ -193,6 +201,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
),
metadata,
},
resolver: zodResolver(
z
.object({
// Length if string, is converted to a number or it can be a number
// Make it optional because it's not submitted from all tabs of the page
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
})
// TODO: Add schema for other fields later.
.passthrough()
),
});
const appsMetadata = formMethods.getValues("metadata")?.apps;
@@ -219,14 +237,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
/>
),
availability: <AvailabilityTab isTeamEvent={!!team} />,
team: (
<EventTeamTab
eventType={eventType}
teamMembers={teamMembers}
team={team}
currentUserMembership={props.currentUserMembership}
/>
),
team: <EventTeamTab eventType={eventType} teamMembers={teamMembers} team={team} />,
limits: <EventLimitsTab eventType={eventType} />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
recurring: <EventRecurringTab eventType={eventType} />,
@@ -237,6 +248,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
workflows={eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow)}
/>
),
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
} as const;
return (
@@ -248,8 +260,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
team={team}
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows"}
currentUserMembership={props.currentUserMembership}>
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={currentUserMembership}>
<Form
form={formMethods}
id="event-type-form"
@@ -283,7 +295,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
if (metadata?.multipleDuration.length < 1) {
throw new Error(t("event_setup_multiple_duration_error"));
} else {
if (input.length && !metadata?.multipleDuration?.includes(input.length)) {
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
throw new Error(t("event_setup_multiple_duration_default_error"));
}
}
@@ -314,6 +326,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
);
};
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery(
{ id: props.type },
{
initialData: props.initialData,
}
);
if (isLoading || !data) return null;
return <EventTypePage {...data} />;
};
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const { req, query } = context;
const session = await getSession({ req });
@@ -335,233 +359,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
};
}
const userSelect = Prisma.validator<Prisma.UserSelect>()({
name: true,
username: true,
id: true,
avatar: true,
email: true,
locale: true,
defaultScheduleId: true,
});
const rawEventType = await prisma.eventType.findFirst({
where: {
AND: [
{
OR: [
{
users: {
some: {
id: session.user.id,
},
},
},
{
team: {
members: {
some: {
userId: session.user.id,
},
},
},
},
{
userId: session.user.id,
},
],
},
{
id: typeParam,
},
],
},
select: {
id: true,
title: true,
slug: true,
description: true,
length: true,
hidden: true,
locations: true,
eventName: true,
customInputs: true,
timeZone: true,
periodType: true,
metadata: true,
periodDays: true,
periodStartDate: true,
periodEndDate: true,
periodCountCalendarDays: true,
requiresConfirmation: true,
recurringEvent: true,
hideCalendarNotes: true,
disableGuests: true,
minimumBookingNotice: true,
beforeEventBuffer: true,
afterEventBuffer: true,
slotInterval: true,
hashedLink: true,
bookingLimits: true,
successRedirectUrl: true,
currency: true,
team: {
select: {
id: true,
slug: true,
members: {
where: {
accepted: true,
},
select: {
role: true,
user: {
select: userSelect,
},
},
},
try {
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
return {
props: {
session,
type: typeParam,
trpcState: ssr.dehydrate(),
initialData: {
eventType: res.eventType,
locationOptions: res.locationOptions,
team: res.team,
teamMembers: res.teamMembers,
currentUserMembership: res.currentUserMembership,
},
},
users: {
select: userSelect,
},
schedulingType: true,
schedule: {
select: {
id: true,
},
},
userId: true,
price: true,
destinationCalendar: true,
seatsPerTimeSlot: true,
seatsShowAttendees: true,
workflows: {
include: {
workflow: {
include: {
activeOn: {
select: {
eventType: {
select: {
id: true,
title: true,
},
},
},
},
steps: true,
},
},
},
},
},
});
if (!rawEventType) {
};
} catch (err) {
return {
notFound: true,
};
}
const credentials = await prisma.credential.findMany({
where: {
userId: session.user.id,
app: {
enabled: true,
},
},
select: {
id: true,
type: true,
key: true,
userId: true,
appId: true,
invalid: true,
},
});
const { locations, metadata, ...restEventType } = rawEventType;
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
const newMetadata = EventTypeMetaDataSchema.parse(metadata || {})!;
const apps = newMetadata.apps || {};
const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata };
newMetadata.apps = {
...apps,
stripe: {
...getStripeAppData(eventTypeWithParsedMetadata, true),
currency:
(
credentials.find((integration) => integration.type === "stripe_payment")
?.key as unknown as StripeData
)?.default_currency || "usd",
},
giphy: getEventTypeAppData(eventTypeWithParsedMetadata, "giphy", true),
rainbow: getEventTypeAppData(eventTypeWithParsedMetadata, "rainbow", true),
};
// TODO: How to extract metadata schema from _EventTypeModel to be able to parse it?
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
const parsedMetaData = newMetadata;
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
const eventType = {
...restEventType,
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
locations: locations as unknown as LocationObject[],
metadata: parsedMetaData,
customInputs: parsedCustomInputs,
};
// backwards compat
if (eventType.users.length === 0 && !eventType.team) {
const fallbackUser = await prisma.user.findUnique({
where: {
id: session.user.id,
},
select: userSelect,
});
if (!fallbackUser) throw Error("The event type doesn't have user and no fallback user was found");
eventType.users.push(fallbackUser);
}
const currentUser = eventType.users.find((u) => u.id === session.user.id);
const t = await getTranslation(currentUser?.locale ?? "en", "common");
const integrations = await getEnabledApps(credentials);
const locationOptions = getLocationOptions(integrations, t);
const eventTypeObject = Object.assign({}, eventType, {
periodStartDate: eventType.periodStartDate?.toString() ?? null,
periodEndDate: eventType.periodEndDate?.toString() ?? null,
});
const teamMembers = eventTypeObject.team
? eventTypeObject.team.members.map((member) => {
const user = member.user;
user.avatar = `${CAL_URL}/${user.username}/avatar.png`;
return user;
})
: [];
// Find the current users memebership so we can check role to enable/disable deletion.
// Sets to null if no membership is found - this must mean we are in a none team event type
const currentUserMembership =
eventTypeObject.team?.members.find((el) => el.user.id === session.user.id) ?? null;
return {
props: {
session,
eventType: eventTypeObject,
locationOptions,
team: eventTypeObject.team || null,
teamMembers,
currentUserMembership,
trpcState: ssr.dehydrate(),
},
};
};
export default EventTypePage;
export default EventTypePageWrapper;
+18 -21
View File
@@ -4,7 +4,10 @@ import Link from "next/link";
import { useRouter } from "next/router";
import React, { Fragment, useEffect, useState } from "react";
import CreateEventTypeButton from "@calcom/features/eventtypes/components/CreateEventTypeButton";
import {
CreateEventTypeButton,
EventTypeDescriptionLazy as EventTypeDescription,
} from "@calcom/features/eventtypes/components";
import { APP_NAME, CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
@@ -22,7 +25,6 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
EventTypeDescription,
Icon,
Shell,
showToast,
@@ -76,7 +78,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
className="hidden font-normal leading-4 text-gray-600 sm:inline"
data-testid={"event-type-slug-" + type.id}>{`/${group.profile.slug}/${type.slug}`}</small>
{readOnly && (
<span className="rtl:mr-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2">
<span className="rtl:ml-2inline items-center rounded-sm bg-gray-100 px-1.5 py-0.5 text-xs font-medium text-gray-800 ltr:ml-2 ltr:mr-2">
{t("readonly") as string}
</span>
)}
@@ -177,22 +179,17 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
}
// inject selection data into url for correct router history
const openModal = (group: EventTypeGroup, type: EventType) => {
const openDuplicateModal = (eventType: EventType) => {
const query = {
...router.query,
dialog: "new-eventtype",
eventPage: group.profile.slug,
title: type.title,
slug: type.slug,
description: type.description,
length: type.length,
type: type.schedulingType,
teamId: group.teamId,
locations: encodeURIComponent(JSON.stringify(type.locations)),
dialog: "duplicate-event-type",
title: eventType.title,
description: eventType.description,
slug: eventType.slug,
id: eventType.id,
length: eventType.length,
};
if (!group.teamId) {
delete query.teamId;
}
router.push(
{
pathname: router.pathname,
@@ -261,7 +258,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
const calLink = `${CAL_URL}/${embedLink}`;
return (
<li key={type.id}>
<div className="flex w-full items-center justify-between hover:bg-neutral-50">
<div className="flex w-full items-center justify-between hover:bg-gray-50">
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
{!(firstItem && firstItem.id === type.id) && (
<button
@@ -361,7 +358,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
type="button"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy}
onClick={() => openModal(group, type)}>
onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string}
</DropdownItem>
</DropdownMenuItem>
@@ -397,7 +394,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
</div>
</div>
</div>
<div className="min-w-9 mr-5 flex sm:hidden">
<div className="min-w-9 mx-5 flex sm:hidden">
<Dropdown>
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
<Button type="button" size="icon" color="secondary" StartIcon={Icon.FiMoreHorizontal} />
@@ -469,7 +466,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
className="w-full rounded-none"
data-testid={"event-type-duplicate-" + type.id}
StartIcon={Icon.FiCopy}
onClick={() => openModal(group, type)}>
onClick={() => openDuplicateModal(type)}>
{t("duplicate") as string}
</Button>
</DropdownMenuItem>
@@ -534,7 +531,7 @@ const EventTypeListHeading = ({
<a className="font-bold">{profile?.name || ""}</a>
</Link>
{membershipCount && teamId && (
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 rtl:mr-2">
<span className="relative -top-px text-xs text-neutral-500 ltr:ml-2 ltr:mr-2 rtl:ml-2">
<Link href={`/settings/teams/${teamId}/members`}>
<a>
<Badge variant="gray">
@@ -10,7 +10,7 @@ function AdminAppsView() {
const { t } = useLocale();
return (
<>
<Meta title={t("apps")} description={t("apps_description")} />
<Meta title={t("apps")} description={t("admin_apps_description")} />
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
</>
);
@@ -21,7 +21,7 @@ function AdminView() {
const enteredUsername = usernameRef.current?.value.toLowerCase();
signIn("impersonation-auth", { username: enteredUsername });
}}>
<div className="flex items-center space-x-2">
<div className="flex items-center space-x-2 rtl:space-x-reverse">
<TextField
containerClassName="w-full"
name={t("user_impersonation_heading")}
@@ -16,7 +16,7 @@ import {
getSettingsLayout as getLayout,
Icon,
Meta,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -177,7 +177,9 @@ const AppearanceView = () => {
<div className="flex w-full text-sm">
<div className="mr-1 flex-grow">
<div className="flex items-center">
<p className="mr-2 font-semibold">{t("disable_cal_branding", { appName: APP_NAME })}</p>
<p className="font-semibold ltr:mr-2 rtl:ml-2">
{t("disable_cal_branding", { appName: APP_NAME })}
</p>
<Badge variant="gray">{t("pro")}</Badge>
</div>
<p className="mt-0.5 text-gray-600">{t("removes_cal_branding", { appName: APP_NAME })}</p>
@@ -189,7 +191,7 @@ const AppearanceView = () => {
onCheckedChange={(checked) =>
formMethods.setValue("hideBranding", checked, { shouldDirty: true })
}
checked={value}
checked={!session.data?.user.belongsToActiveTeam ? false : value}
/>
</div>
</div>
@@ -156,7 +156,7 @@ const CalendarsView = () => {
)
}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2">
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
<Link href={"/apps/" + item.integration.slug}>
{item.integration.name || item.integration.title}
</Link>
@@ -82,7 +82,7 @@ const ConferencingLayout = () => {
app.logo && <img className="h-10 w-10" src={app.logo} alt={app.title} />
}
<div className="flex-grow truncate pl-2">
<ListItemTitle component="h3" className="mb-1 space-x-2">
<ListItemTitle component="h3" className="mb-1 space-x-2 rtl:space-x-reverse">
<h3 className="truncate text-sm font-medium text-neutral-900">{app.title}</h3>
</ListItemTitle>
<ListItemText component="p">{app.description}</ListItemText>
+11 -5
View File
@@ -70,6 +70,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: () => {
reset(getValues());
showToast(t("settings_updated_successfully"), "success");
},
onError: () => {
@@ -119,7 +120,12 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
},
},
});
const {
formState: { isDirty, isSubmitting },
reset,
getValues,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<Form
form={formMethods}
@@ -160,7 +166,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
id="timezone"
value={value}
onChange={(event) => {
if (event) formMethods.setValue("timeZone", event.value);
if (event) formMethods.setValue("timeZone", event.value, { shouldDirty: true });
}}
/>
</>
@@ -178,7 +184,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
value={value}
options={timeFormatOptions}
onChange={(event) => {
if (event) formMethods.setValue("timeFormat", { ...event });
if (event) formMethods.setValue("timeFormat", { ...event }, { shouldDirty: true });
}}
/>
</>
@@ -199,13 +205,13 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
value={value}
options={weekStartOptions}
onChange={(event) => {
if (event) formMethods.setValue("weekStart", { ...event });
if (event) formMethods.setValue("weekStart", { ...event }, { shouldDirty: true });
}}
/>
</>
)}
/>
<Button color="primary" type="submit" className="mt-8">
<Button disabled={isDisabled} color="primary" type="submit" className="mt-8">
<>{t("update")}</>
</Button>
</Form>
@@ -340,7 +340,7 @@ const ProfileForm = ({
render={({ field: { value } }) => (
<>
<Avatar alt="" imageSrc={value} gravatarFallbackMd5={emailMd5} size="lg" />
<div className="ml-4">
<div className="ltr:ml-4 rtl:mr-4">
<ImageUploader
target="avatar"
id="avatar-upload"
@@ -23,8 +23,24 @@ const ProfileImpersonationView = () => {
const mutation = trpc.viewer.updateProfile.useMutation({
onSuccess: () => {
showToast(t("profile_updated_successfully"), "success");
reset(getValues());
},
onError: (error) => {
onSettled: () => {
utils.viewer.me.invalidate();
},
onMutate: async ({ disableImpersonation }) => {
await utils.viewer.me.cancel();
const previousValue = utils.viewer.me.getData();
if (previousValue && disableImpersonation) {
utils.viewer.me.setData(undefined, { ...previousValue, disableImpersonation });
}
return { previousValue };
},
onError: (error, variables, context) => {
if (context?.previousValue) {
utils.viewer.me.setData(undefined, context.previousValue);
}
showToast(`${t("error")}, ${error.message}`, "error");
},
});
@@ -36,10 +52,13 @@ const ProfileImpersonationView = () => {
});
const {
formState: { isSubmitting },
formState: { isSubmitting, isDirty },
setValue,
reset,
getValues,
} = formMethods;
const isDisabled = isSubmitting || !isDirty;
return (
<>
<Meta title={t("impersonation")} description={t("impersonation_description")} />
@@ -47,14 +66,12 @@ const ProfileImpersonationView = () => {
form={formMethods}
handleSubmit={({ disableImpersonation }) => {
mutation.mutate({ disableImpersonation });
utils.viewer.me.invalidate();
}}>
<div className="flex space-x-3">
<Switch
{...formMethods.register("disableImpersonation")}
defaultChecked={!user?.disableImpersonation}
onCheckedChange={(e) => {
setValue("disableImpersonation", !e);
setValue("disableImpersonation", !e, { shouldDirty: true });
}}
fitToHeight={true}
/>
@@ -67,7 +84,7 @@ const ProfileImpersonationView = () => {
</Skeleton>
</div>
</div>
<Button color="primary" className="mt-8" type="submit" disabled={isSubmitting || mutation.isLoading}>
<Button color="primary" className="mt-8" type="submit" disabled={isDisabled}>
{t("update")}
</Button>
</Form>
@@ -79,7 +96,7 @@ ProfileImpersonationView.getLayout = getLayout;
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
const ssr = await ssrInit(context);
await ssr.viewer.me.prefetch();
return {
props: {
trpcState: ssr.dehydrate(),
@@ -18,10 +18,9 @@ import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
import { ssrInit } from "@server/lib/ssr";
const SkeletonLoader = ({ title, description }: { title: string; description: string }) => {
const SkeletonLoader = () => {
return (
<SkeletonContainer>
<Meta title={title} description={description} />
<div className="mt-6 mb-8 space-y-6 divide-y">
<div className="flex items-center">
<SkeletonButton className="mr-6 h-8 w-20 rounded-md p-5" />
@@ -41,7 +40,7 @@ const TwoFactorAuthView = () => {
const [enableModalOpen, setEnableModalOpen] = useState(false);
const [disableModalOpen, setDisableModalOpen] = useState(false);
if (isLoading) return <SkeletonLoader title={t("2fa")} description={t("2fa_description")} />;
if (isLoading) return <SkeletonLoader />;
return (
<>
+12 -3
View File
@@ -6,6 +6,7 @@ import { FormProvider, SubmitHandler, useForm } from "react-hook-form";
import LicenseRequired from "@calcom/features/ee/common/components/v2/LicenseRequired";
import { isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Alert, Button, EmailField, PasswordField, TextField } from "@calcom/ui";
import { HeadSeo } from "@calcom/web/components/seo/head-seo";
@@ -26,6 +27,8 @@ type FormValues = {
export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
const router = useRouter();
const telemetry = useTelemetry();
const methods = useForm<FormValues>({
defaultValues: prepopulateFormValues,
});
@@ -53,8 +56,12 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
})
.then(handleErrors)
.then(async () => {
await signIn("Cal.com", {
callbackUrl: router.query.callbackUrl ? `${WEBAPP_URL}/${router.query.callbackUrl}` : WEBAPP_URL,
telemetry.event(telemetryEventTypes.login, collectPageParameters());
await signIn<"credentials">("credentials", {
...data,
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
});
})
.catch((err) => {
@@ -111,7 +118,9 @@ export default function Signup({ prepopulateFormValues }: inferSSRProps<typeof g
className="w-5/12 justify-center"
onClick={() =>
signIn("Cal.com", {
callbackUrl: (`${WEBAPP_URL}/${router.query.callbackUrl}` || "") as string,
callbackUrl: router.query.callbackUrl
? `${WEBAPP_URL}/${router.query.callbackUrl}`
: `${WEBAPP_URL}/getting-started`,
})
}>
{t("login_instead")}
+6 -7
View File
@@ -5,13 +5,14 @@ import { useRouter } from "next/router";
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 { useLocale } from "@calcom/lib/hooks/useLocale";
import useTheme from "@calcom/lib/hooks/useTheme";
import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
import { Avatar, Button, EventTypeDescription, Icon } from "@calcom/ui";
import { Avatar, Button, Icon } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
@@ -37,18 +38,18 @@ function TeamPage({ team }: TeamPageProps) {
}, [telemetry, router.asPath]);
const EventTypes = () => (
<ul className="">
<ul className="rounded-md border border-neutral-200 dark:border-neutral-700">
{team.eventTypes.map((type, index) => (
<li
key={index}
className={classNames(
"dark:bg-darkgray-100 dark:border-darkgray-200 group relative rounded-sm border border-neutral-200 bg-white hover:bg-gray-50 dark:hover:border-neutral-600",
"dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600",
!isEmbed && "bg-white"
)}>
<Link href={`/team/${team.slug}/${type.slug}`}>
<a 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">
<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} />
@@ -96,9 +97,7 @@ function TeamPage({ team }: TeamPageProps) {
{(showMembers.isOn || !team.eventTypes.length) && <Team team={team} />}
{!showMembers.isOn && team.eventTypes.length > 0 && (
<div className="mx-auto max-w-3xl ">
<div className="dark:border-darkgray-300 rounded-md border">
<EventTypes />
</div>
<EventTypes />
<div className="relative mt-12">
<div className="absolute inset-0 flex items-center" aria-hidden="true">
<div className="dark:border-darkgray-300 w-full border-t border-gray-200" />
+5 -2
View File
@@ -14,8 +14,11 @@ function Teams() {
heading={t("teams")}
subtitle={t("create_manage_teams_collaborative")}
CTA={
<Button type="button" href={`${WEBAPP_URL}/settings/teams/new`}>
<Icon.FiPlus className="inline-block h-3.5 w-3.5 text-white group-hover:text-black ltr:mr-2 rtl:ml-2" />
<Button
size="fab"
StartIcon={Icon.FiPlus}
type="button"
href={`${WEBAPP_URL}/settings/teams/new?returnTo=/teams`}>
{t("new")}
</Button>
}>
+54
View File
@@ -0,0 +1,54 @@
import { expect } from "@playwright/test";
import dayjs from "@calcom/dayjs";
import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Availablity tests", () => {
test.beforeEach(async ({ page, users }) => {
const user = await users.create();
await user.login();
await page.goto("/availability");
// We wait until loading is finished
await page.waitForSelector('[data-testid="schedules"]');
});
test.afterEach(async ({ users }) => {
await users.deleteAll();
});
test("Date Overrides", async ({ page }) => {
await test.step("Can add a date override", async () => {
await page.locator('[data-testid="schedules"] > li a').click();
await page.locator('[data-testid="add-override"]').click();
await page.locator('[id="modal-title"]').waitFor();
await page.locator('[data-testid="incrementMonth"]').click();
await page.locator('[data-testid="day"][data-disabled="false"]').nth(0).click();
await page.locator('[data-testid="date-override-mark-unavailable"]').click();
await page.locator('[data-testid="add-override-submit-btn"]').click();
await expect(page.locator('[data-testid="date-overrides-list"] > li')).toHaveCount(1);
await page.locator('[form="availability-form"][type="submit"]').click();
});
await test.step("Date override is displayed in troubleshooter", async () => {
const response = await page.waitForResponse("**/api/trpc/viewer.availability.schedule.update?batch=1");
const json = await response.json();
// @ts-expect-error trust me bro
const date = json[0].result.data.json.schedule.availability.find((a) => !!a.date);
const troubleshooterURL = `/availability/troubleshoot?date=${dayjs(date.date).format("YYYY-MM-DD")}`;
await page.goto(troubleshooterURL);
await expect(page.locator('[data-testid="troubleshooter-busy-time"]')).toHaveCount(1);
});
});
test("Availablity pages", async ({ page }) => {
await test.step("Can add a new schedule", async () => {
await page.locator('[data-testid="new-schedule"]').click();
await page.locator('[id="name"]').fill("More working hours");
page.locator('[type="submit"]').click();
await expect(page.locator("[data-testid=availablity-title]")).toHaveValue("More working hours");
});
});
});
+5 -5
View File
@@ -8,10 +8,10 @@ import { test } from "./lib/fixtures";
test.describe.configure({ mode: "parallel" });
test.describe("Event Types tests", () => {
test.describe("pro user", () => {
test.describe("user", () => {
test.beforeEach(async ({ page, users }) => {
const proUser = await users.create();
await proUser.login();
const user = await users.create();
await user.login();
await page.goto("/event-types");
// We wait until loading is finished
await page.waitForSelector('[data-testid="event-types"]');
@@ -101,13 +101,13 @@ test.describe("Event Types tests", () => {
const params = new URLSearchParams(url);
expect(params.get("title")).toBe(firstTitle);
expect(params.get("slug")).toBe(firstSlug);
expect(params.get("slug")).toContain(firstSlug);
const formTitle = await page.inputValue("[name=title]");
const formSlug = await page.inputValue("[name=slug]");
expect(formTitle).toBe(firstTitle);
expect(formSlug).toBe(firstSlug);
expect(formSlug).toContain(firstSlug);
});
test("edit first event", async ({ page }) => {
const $eventTypes = page.locator("[data-testid=event-types] > li a");
+1 -1
View File
@@ -247,7 +247,7 @@ const createUser = async (
opts?: CustomUserOpts | null
): Promise<PrismaType.UserCreateInput> => {
// build a unique name for our user
const uname = `${opts?.username}-${workerInfo.workerIndex}-${Date.now()}`;
const uname = `${opts?.username || "user"}-${workerInfo.workerIndex}-${Date.now()}`;
return {
username: uname,
name: opts?.name,
+2
View File
@@ -57,6 +57,7 @@ test("add webhook & test that creating an event triggers a webhook call", async
attendee.timeZone = dynamic;
attendee.language = dynamic;
}
body.payload.organizer.id = dynamic;
body.payload.organizer.email = dynamic;
body.payload.organizer.timeZone = dynamic;
body.payload.organizer.language = dynamic;
@@ -67,6 +68,7 @@ test("add webhook & test that creating an event triggers a webhook call", async
body.payload.eventTypeId = dynamic;
body.payload.videoCallData = dynamic;
body.payload.appsStatus = dynamic;
body.payload.metadata.videoCallUrl = dynamic;
// if we change the shape of our webhooks, we can simply update this by clicking `u`
// console.log("BODY", body);
@@ -1 +1 @@
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
{"triggerEvent":"BOOKING_CREATED","createdAt":"[redacted/dynamic]","payload":{"type":"30 min","title":"30 min between Nameless and Test Testson","description":"","additionalNotes":"","customInputs":{},"startTime":"[redacted/dynamic]","endTime":"[redacted/dynamic]","organizer":{"id":"[redacted/dynamic]","name":"Nameless","email":"[redacted/dynamic]","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"},"attendees":[{"email":"test@example.com","name":"Test Testson","timeZone":"[redacted/dynamic]","language":"[redacted/dynamic]"}],"location":"[redacted/dynamic]","destinationCalendar":null,"hideCalendarNotes":false,"requiresConfirmation":"[redacted/dynamic]","eventTypeId":"[redacted/dynamic]","seatsShowAttendees":false,"uid":"[redacted/dynamic]","videoCallData":"[redacted/dynamic]","appsStatus":"[redacted/dynamic]","eventTitle":"30 min","eventDescription":null,"price":0,"currency":"usd","length":30,"bookingId":"[redacted/dynamic]","metadata":{"videoCallUrl":"[redacted/dynamic]"},"status":"ACCEPTED","additionalInformation":"[redacted/dynamic]"}}
+5
View File
@@ -0,0 +1,5 @@
<svg stroke="#888888" fill="none" stroke-width="2.5" viewBox="0 0 24 24" stroke-linecap="round" stroke-linejoin="round" height="1em" width="1em" xmlns="http://www.w3.org/2000/svg">
<path d="M18 13v6a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V8a2 2 0 0 1 2-2h6"></path>
<polyline points="15 3 21 3 21 9"></polyline>
<line x1="10" y1="14" x2="21" y2="3"></line>
</svg>

After

Width:  |  Height:  |  Size: 360 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 197 KiB

After

Width:  |  Height:  |  Size: 53 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 166 KiB

After

Width:  |  Height:  |  Size: 35 KiB

+70 -2
View File
@@ -52,6 +52,7 @@
"still_waiting_for_approval": "يوجد حدث لا يزال في انتظار الموافقة",
"event_is_still_waiting": "لا يزال طلب الحدث في الانتظار: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "لا يوجد مزيد من النتائج",
"no_results": "لا نتائج",
"load_more_results": "تحميل المزيد من النتائج",
"integration_meeting_id": "مُعرّف اجتماع {{integrationName}}:‏ {{meetingId}}",
"confirmed_event_type_subject": "تم التأكيد: {{eventType}} مع {{name}} في {{date}}",
@@ -248,6 +249,7 @@
"add_to_calendar": "إضافة إلى التقويم",
"add_another_calendar": "إضافة تقويم آخر",
"other": "آخر",
"email_sign_in_subject": "رابط تسجيل الدخول بك لـ{{appName}}",
"emailed_you_and_attendees": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة للتقويم عبر البريد الإلكتروني تتضمن كل التفاصيل.",
"emailed_you_and_attendees_recurring": "لقد أرسلنا إليك وإلى الحضور الآخرين دعوة تقويم عبر البريد الإلكتروني لأول هذه الأحداث المتكررة.",
"emailed_you_and_any_other_attendees": "تم إرسال هذه المعلومات إليك وإلى الحضور الآخرين عبر البريد الإلكتروني.",
@@ -420,6 +422,7 @@
"current_incorrect_password": "كلمة المرور الحالية غير صحيحة",
"password_hint_caplow": "مزيج من الحروف الكبيرة والحروف الصغيرة",
"password_hint_min": "8 أحرف كحد أدنى",
"password_hint_admin_min": "15 محرفًا كحد أدنى",
"password_hint_num": "يحتوي على رقم واحد على الأقل",
"invalid_password_hint": "يجب أن تتألف كلمة المرور على الأقل من 7 أحرف، ورقم واحد على الأقل، ومزيج من الحروف الكبيرة والصغيرة",
"incorrect_password": "كلمة المرور غير صحيحة.",
@@ -463,11 +466,14 @@
"booking_confirmation": "قم بتأكيد {{eventTypeTitle}} مع {{profileName}}",
"booking_reschedule_confirmation": "أعد جدولة {{eventTypeTitle}} مع {{profileName}}",
"in_person_meeting": "الرابط أو الاجتماع الشخصي",
"attendeeInPerson": "شخصيًا (عنوان من سيحضر)",
"inPerson": "شخصيًا (عنوان المنظم)",
"link_meeting": "رابط الاجتماع",
"phone_call": "رقم هاتف الحضور",
"your_number": "رقم هاتفك",
"phone_number": "رقم الهاتف",
"attendee_phone_number": "رقم هاتف الحضور",
"organizer_phone_number": "رقم هاتف المنظم",
"host_phone_number": "رقم هاتفك",
"enter_phone_number": "أدخل رقم الهاتف",
"reschedule": "إعادة الجدولة",
@@ -554,6 +560,10 @@
"collective": "جماعي",
"collective_description": "جدولة الاجتماعات عندما يكون جميع أعضاء الفريق المحددين متاحين.",
"duration": "المدة",
"available_durations": "المدد المتاحة",
"default_duration": "المدة الافتراضية",
"default_duration_no_options": "يرجى اختيار المدد المتاحة أولاً",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "الدقائق",
"round_robin": "الترتيب الدوري",
"round_robin_description": "نقل الاجتماعات بشكل دوري بين أعضاء الفريق المتعددين.",
@@ -619,6 +629,7 @@
"teams": "الفرق",
"team": "فريق",
"team_billing": "الفوترة الخاصة بالفريق",
"team_billing_description": "إدارة الفواتير لفريقك",
"upgrade_to_flexible_pro_title": "لقد قمنا بتغيير الفوترة الخاصة بالفرق",
"upgrade_to_flexible_pro_message": "يوجد أعضاء في فريقك من دون مقاعد. قم بترقية خطة Pro خاصتك لتوفير المقاعد الناقصة.",
"changed_team_billing_info": "بدءًا من يناير 2022، أصبحنا نفرض الرسوم على مقاعد أعضاء الفريق بشكل منفرد. حاليًا، أعضاء فريقك الذين كانت لديهم خطة Pro مجانية أصبحوا في فترة تجريبية مدتها 14 يومًا. وبمجرد انتهاء الفترة التجريبية الخاصة بهم، سيجري إخفاء هؤلاء الأعضاء من فريقك ما لم تقم بالترقية الآن.",
@@ -694,6 +705,7 @@
"hide_event_type": "إخفاء نوع الحدث",
"edit_location": "تعديل الموقع",
"into_the_future": "في المستقبل",
"when_booked_with_less_than_notice": "عند الحجز بأقل من إشعار <time></time>",
"within_date_range": "خلال فترة زمنية",
"indefinitely_into_future": "إلى أجل غير مسمى في المستقبل",
"add_new_custom_input_field": "إضافة خانة إدخال مخصص جديدة",
@@ -713,6 +725,8 @@
"delete_account_confirmation_message": "هل تريد بالتأكيد حذف حسابك على {{appName}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
"integrations": "التكاملات",
"apps": "التطبيقات",
"apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
"apps_listing": "قائمة التطبيقات",
"category_apps": "تطبيقات {{category}}",
"app_store": "متجر التطبيقات",
"app_store_description": "الربط بين الناس، والتكنولوجيا، ومكان العمل.",
@@ -736,6 +750,7 @@
"toggle_calendars_conflict": "قم بتبديل التقويمات التي تريد التحقق من وجود تضاربات فيها لمنع حدوث حجز مزدوج.",
"select_destination_calendar": "إنشاء أحداث في",
"connect_additional_calendar": "ربط رزنامة إضافية",
"calendar_updated_successfully": "تم تحديث الرزنامة بنجاح",
"conferencing": "المؤتمرات عبر الفيديو",
"calendar": "التقويم",
"payments": "المدفوعات",
@@ -768,6 +783,7 @@
"trending_apps": "التطبيقات الرائجة",
"explore_apps": "تطبيقات {{category}}",
"installed_apps": "التطبيقات المثبتة",
"free_to_use_apps": "متاح",
"no_category_apps": "لا توجد تطبيقات {{category}}",
"no_category_apps_description_calendar": "إضافة تطبيق تقويم للتحقق من أي تضارب لمنع أي حجوزات مزدوجة",
"no_category_apps_description_conferencing": "جرب إضافة تطبيق مؤتمرات لإنشاء مكالمات فيديو مع عملائك",
@@ -806,6 +822,8 @@
"verify_wallet": "تأكيد المحفظة",
"connect_metamask": "توصيل Metamask",
"create_events_on": "إنشاء أحداث في",
"enterprise_license": "هذه هي ميزة للمؤسسات",
"enterprise_license_description": "لتمكين هذه الميزة، احصل على مفتاح نشر في وحدة التحكم {{consoleUrl}} وأضفه إلى وحدة التحكم الخاصة بك. nv باسم CALCOM_LICENSE_KEY. إذا كان لدى فريقك بالفعل ترخيص، يرجى الاتصال بـ {{supportMail}} للحصول على المساعدة.",
"missing_license": "الترخيص مفقود",
"signup_requires": "يلزم ترخيص تجاري",
"signup_requires_description": "لا تقدم شركة {{companyName}} حاليًا إصدارًا مجانيًا مفتوح المصدر لصفحة التسجيل. للحصول على حق الوصول الكامل إلى مكونات التسجيل، يجب أن تحصل على ترخيص تجاري. للاستخدام الشخصي، نوصي باستخدام منصة Prisma Data أو أي واجهة أخرى من واجهات Postgres لإنشاء الحسابات.",
@@ -897,6 +915,7 @@
"user_impersonation_heading": "انتحال شخصية مستخدم",
"user_impersonation_description": "يسمح لفريق الدعم الخاص بنا بتسجيل الدخول مؤقتًا نيابة عنك لمساعدتنا على حل أي مشاكل تبلغ عنها بسرعة.",
"team_impersonation_description": "يسمح لأعضاء فريقك بتسجيل الدخول نيابة عنك مؤقتًا.",
"allow_booker_to_select_duration": "السماح لمن يقوم بالحجز بتحديد المدة",
"impersonate_user_tip": "يتم مراجعة جميع استخدامات هذه الميزة.",
"impersonating_user_warning": "انتحال اسم مستخدم \"{{user}}\".",
"impersonating_stop_instructions": "<0>انقر هنا لإيقاف </0>.",
@@ -1014,6 +1033,9 @@
"error_removing_app": "خطأ في إزالة التطبيق",
"web_conference": "مؤتمر عبر الإنترنت",
"requires_confirmation": "يتطلب التأكيد",
"always_requires_confirmation": "دائمًا",
"requires_confirmation_threshold": "يتطلب تأكيد إذا تم حجزه مع إشعار $t({{unit}}_timeUnit){{time}}",
"may_require_confirmation": "قد يتطلب التأكيد",
"nr_event_type_one": "{{count}} نوع حدث",
"nr_event_type_other": "{{count}} من أنواع الحدث",
"add_action": "إضافة إجراء",
@@ -1101,6 +1123,9 @@
"event_limit_tab_description": "ما تواتر مرات الحجز",
"event_advanced_tab_description": "إعدادات الرزنامة والمزيد...",
"event_advanced_tab_title": "متقدم",
"event_setup_multiple_duration_error": "إعداد الحدث: تستلزم فترات متعددة خيارًا واحدًا على الأقل.",
"event_setup_multiple_duration_default_error": "إعداد الحدث: الرجاء تحديد مدة افتراضية صالحة.",
"event_setup_booking_limits_error": "يجب أن تكون حدود الحجز بترتيب تصاعدي. [يوم،أسبوع،شهر،سنة]",
"select_which_cal": "اختر أي رزنامة تريد إضافة الحجز إليها",
"custom_event_name": "اسم حدث مخصص",
"custom_event_name_description": "إنشاء أسماء أحداث مخصصة لعرضها في حدث الرزنامة",
@@ -1154,8 +1179,11 @@
"invoices": "الفواتير",
"embeds": "التضمينات",
"impersonation": "انتحال الشخصية",
"impersonation_description": "إعدادات إدارة انتحال شخصية مستخدم",
"users": "المستخدمون",
"profile_description": "إدارة الإعدادات لملفك الشخصي على {{appName}}",
"users_description": "يمكنك هنا العثور على قائمة بجميع المستخدمين",
"users_listing": "قائمة المستخدمين",
"general_description": "إدارة الإعدادات للغة والمنطقة الزمنية الخاصة بك",
"calendars_description": "قم بتهيئة كيفية تفاعل أنواع الأحداث مع التقويمات الخاصة بك",
"appearance_description": "إدارة إعدادات مظهر الحجز الخاص بك",
@@ -1360,8 +1388,9 @@
"number_sms_notifications": "رقم الهاتف (إشعارات الرسائل النصية)",
"attendee_email_workflow": "اسم الحاضر",
"attendee_email_info": "البريد الإلكتروني للشخص الحجز",
"kbar_search_placeholder": "اكتب أمرًا أو بحثًا...",
"invalid_credential": "لا! يبدو أن الصلاحية انتهت أو تم إلغاؤها. يرجى إعادة التثبيت مرة أخرى.",
"choose_common_schedule_team_event": "اختر جدولاً زمنياً مشتركاً",
"choose_common_schedule_team_event": "اختر جدولًا زمنيًا مشتركًا",
"choose_common_schedule_team_event_description": "قم بتمكين هذا الخيار إذا كنت ترغب في استخدام جدول زمني مشترك بين المضيفين. عند تعطيل هذا الخيار، سيتم حجز كل مضيف على أساس جدوله الافتراضي.",
"reason": "السبب",
"sender_id": "معرّف المرسل",
@@ -1370,5 +1399,44 @@
"test_preview": "اختبار المعاينة",
"route_to": "التوجيه إلى",
"test_preview_description": "اختبار نموذج التوجيه الخاص بك دون تقديم أي بيانات",
"test_routing": "اختبار التوجيه"
"test_routing": "اختبار التوجيه",
"payment_app_disabled": "قام المشرف بتعطيل تطبيق دفع",
"edit_event_type": "تعديل نوع الحدث",
"collective_scheduling": "جدولة جماعية",
"make_it_easy_to_book": "سهّل على فريقك الحجز عندما يكون الجميع متاحين.",
"find_the_best_person": "ابحث عن أفضل شخص متوفر، وقم بالتدوير بين أعضاء فريقك.",
"fixed_round_robin": "روبين دائري ثابت",
"add_one_fixed_attendee": "أضف حاضرًا ثابتًا وروبين مستدير من خلال عدد من الحاضرين.",
"calcom_is_better_with_team": "Cal.com أفضل مع الفرق",
"add_your_team_members": "أضف أعضاء فريقك إلى أنواع الأحداث الخاصة بك. استخدم الجدولة الجماعية لإدراج الجميع أو العثور على أنسب شخص مع جدولة روبن الدائرية.",
"booking_limit_reached": "تم الوصول إلى الحد الأقصى للحجز لهذا النوع من الأحداث",
"admin_has_disabled": "قام المشرف بتعطيل {{appName}}",
"disabled_app_affects_event_type": "قام المشرف بتعطيل {{appName}} مما يؤثر على نوع الحدث {{eventType}} لديك",
"disable_payment_app": "قام المشرف بتعطيل {{appName}} الذي يؤثر على نوع الحدث {{title}} لديك. ولا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
"payment_disabled_still_able_to_book": "لا يزال بإمكان الحاضرين حجز هذا النوع من الأحداث، ولكن لن يُطالبوا بالدفع. يمكنك إخفاء نوع الحدث لمنع حدوث هذا إلى أن يقوم المشرف بتغيير طريقة الدفع لك.",
"app_disabled_with_event_type": "عطّل المشرف {{appName}} مما يؤثر على نوع الحدث {{title}} لديك.",
"app_disabled_video": "عطّل المشرف {{appName}} الذي قد يؤثر على أنواع الأحداث لديك. إذا كان لديك أنواع من الأحداث مع {{appName}} كموقع، سيكون افتراضيًا من أجل فيديو Cal.",
"app_disabled_subject": "تم تعطيل {{appName}}",
"navigate_installed_apps": "الذهاب إلى التطبيقات المثبتة",
"disabled_calendar": "إذا كان لديك تقويم آخر مثبت، ستُضاف الحجوزات الجديدة إليه. إذا لم يكن الأمر كذلك، قم إذن بتوصيل تقويم جديد لكيلا تفوت أي حجوزات جديدة.",
"enable_apps": "تمكين التطبيقات",
"enable_apps_description": "تمكين التطبيقات التي يمكن للمستخدمين دمجها مع Cal.com",
"app_is_enabled": "تم تمكين {{appName}}",
"app_is_disabled": "تم تعطيل {{appName}}",
"keys_have_been_saved": "تم حفظ المفاتيح",
"disable_app": "تعطيل التطبيق",
"disable_app_description": "يمكن لتعطيل هذا التطبيق أن يسبب مشاكل مع كيفية تفاعل المستخدمين مع Cal",
"edit_keys": "تحرير المفاتيح",
"admin_apps_description": "تمكين تطبيقات مثيل Cal الخاص بك",
"no_available_apps": "لا توجد تطبيقات متاحة",
"no_available_apps_description": "الرجاء التأكد من وجود تطبيقات في نشرك تحت 'حزم/متجر تطبيقات'",
"no_apps": "لا توجد تطبيقات مفعلة في هذا المظهر من Cal",
"apps_settings": "إعدادات التطبيقات",
"fill_this_field": "يرجى ملء هذا الحقل",
"options": "خيارات",
"enter_option": "أدخل خيار {{index}}",
"add_an_option": "إضافة خيار",
"radio": "الراديو",
"event_type_duplicate_copy_text": "{{slug}}-نسخ",
"set_as_default": "تعيين كافتراضي"
}
+66 -1
View File
@@ -52,6 +52,7 @@
"still_waiting_for_approval": "Událost stále čeká na schválení",
"event_is_still_waiting": "Požadavek na událost stále čeká: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "Žádné další výsledky",
"no_results": "Žádné výsledky",
"load_more_results": "Načíst další výsledky",
"integration_meeting_id": "{{integrationName}} ID schůzky: {{meetingId}}",
"confirmed_event_type_subject": "Potvrzeno: {{eventType}} s {{name}} během {{date}}",
@@ -248,6 +249,7 @@
"add_to_calendar": "Přidat do kalendáře",
"add_another_calendar": "Přidat další kalendář",
"other": "Ostatní",
"email_sign_in_subject": "Váš přihlašovací odkaz pro {{appName}}",
"emailed_you_and_attendees": "Poslali jsme vám a ostatním účastníkům e-mail s pozvánkou a podrobnostmi.",
"emailed_you_and_attendees_recurring": "Na první z těchto opakovaných událostí jsme vám a ostatním účastníkům poslali e-mail s pozvánkou a podrobnostmi.",
"emailed_you_and_any_other_attendees": "Poslali jsme vám a ostatním účastníkům e-mail s těmito informacemi.",
@@ -420,6 +422,7 @@
"current_incorrect_password": "Aktuální heslo není správné",
"password_hint_caplow": "Kombinace velkých a malých písmen",
"password_hint_min": "Minimální délka 7 znaků",
"password_hint_admin_min": "Minimální délka 15 znaků",
"password_hint_num": "Obsahuje alespoň 1 číslo",
"invalid_password_hint": "Heslo musí být minimálně 7 znaků dlouhé, obsahovat alespoň jedno číslo a obsahovat kombinaci velkých a malých písmen",
"incorrect_password": "Heslo není správné.",
@@ -463,11 +466,14 @@
"booking_confirmation": "Potvrďte {{eventTypeTitle}} skrz účet {{profileName}}",
"booking_reschedule_confirmation": "Přesunout {{eventTypeTitle}} na jindy skrz účet {{profileName}}",
"in_person_meeting": "Odkaz nebo osobní schůzka",
"attendeeInPerson": "Osobně (adresa účastníka)",
"inPerson": "Osobně (adresa organizátora)",
"link_meeting": "Odkaz na schůzku",
"phone_call": "Telefon",
"your_number": "Vaše telefonní číslo",
"phone_number": "Tel. číslo",
"attendee_phone_number": "Telefonní číslo účastníka",
"organizer_phone_number": "Telefonní číslo organizátora",
"host_phone_number": "Vaše telefonní číslo",
"enter_phone_number": "Zadejte telefonní číslo",
"reschedule": "Přesunout na jindy",
@@ -554,6 +560,10 @@
"collective": "Souhrn",
"collective_description": "Naplánovat schůzku na dobu, kdy jsou vybraní členové týmu dostupní.",
"duration": "Doba trvání",
"available_durations": "Dostupné doby trvání",
"default_duration": "Výchozí doba trvání",
"default_duration_no_options": "Nejprve vyberte dostupné doby trvání",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "min",
"round_robin": "Plánování Round Robin",
"round_robin_description": "Schůzky v řadě mezi několika členy týmu.",
@@ -619,6 +629,7 @@
"teams": "Týmy",
"team": "Tým",
"team_billing": "Fakturace týmů",
"team_billing_description": "Správa účtování pro váš tým",
"upgrade_to_flexible_pro_title": "Změnili jsme fakturaci pro týmy",
"upgrade_to_flexible_pro_message": "Ve vašem týmu jsou členové bez místa. Upgradujte svůj Pro plán pro pokrytí chybějících míst.",
"changed_team_billing_info": "Od ledna 2022 účtujeme členy vašeho týmu podle počtu míst. Členové vašeho týmu, kteří využívali tarif Pro zdarma, nyní mají 14 denní zkušební verzi. Jakmile zkušební verze skončí, nebudou tito členové v týmu vidět, pokud nepřejdete na vyšší tarif.",
@@ -694,6 +705,7 @@
"hide_event_type": "Skrýt typ události",
"edit_location": "Upravit polohu",
"into_the_future": "v budoucnu",
"when_booked_with_less_than_notice": "Při rezervaci s předstihem méně než <time></time>",
"within_date_range": "V časovém rozmezí",
"indefinitely_into_future": "Neomezeně do budoucna",
"add_new_custom_input_field": "Přidat nové vlastní pole",
@@ -713,6 +725,7 @@
"delete_account_confirmation_message": "Opravdu chcete odstranit svůj účet na {{appName}}? Všichni, se kterými jste sdíleli odkaz na účet, přijdou o možnost vytvářet rezervace a vy přijdete o uložené předvolby.",
"integrations": "Integrace",
"apps": "Aplikace",
"apps_listing": "Výpis aplikace",
"category_apps": "Aplikace v kategorii {{category}}",
"app_store": "App Store",
"app_store_description": "Propojování lidí, technologií a pracoviště.",
@@ -769,6 +782,7 @@
"trending_apps": "Populární aplikace",
"explore_apps": "Aplikace v kategorii {{category}}",
"installed_apps": "Nainstalované aplikace",
"free_to_use_apps": "Zdarma",
"no_category_apps": "Žádné aplikace v kategorii {{category}}",
"no_category_apps_description_calendar": "Přidejte aplikaci kalendáře, ať máte přehled v zájmu prevence dvojích rezervací",
"no_category_apps_description_conferencing": "Zkuste přidat aplikaci pro konference, která umožní propojit videohovory s vašimi klienty",
@@ -807,6 +821,8 @@
"verify_wallet": "Ověřit peněženku",
"connect_metamask": "Připojit Metamask",
"create_events_on": "Vytvořit události v:",
"enterprise_license": "Jedná se o firemní funkci",
"enterprise_license_description": "Pokud chcete povolit tuto funkci, získejte klíč pro nasazení v konzoli {{consoleUrl}} a přidejte ho do souboru .env jako CALCOM_LICENSE_KEY. Pokud váš tým již licenci má, kontaktujte prosím {{supportMail}} a požádejte o pomoc.",
"missing_license": "Chybějící licence",
"signup_requires": "Je vyžadována komerční licence",
"signup_requires_description": "{{companyName}} v současné době nenabízí bezplatnou open source verzi stránky registrace. Chcete-li získat plný přístup ke komponentám registrace, musíte získat komerční licenci. Pro osobní použití doporučujeme Prisma Data Platform nebo jakékoli jiné Postgres rozhraní pro vytváření účtů.",
@@ -898,6 +914,7 @@
"user_impersonation_heading": "Přizpůsobení uživatele",
"user_impersonation_description": "Náš tým podpory se bude moci dočasně přihlásit místo vás, aby nám pomohl rychle vyřešit jakékoli problémy, které nám nahlásíte.",
"team_impersonation_description": "Členové vašeho týmu se mohou dočasně přihlásit jako vy.",
"allow_booker_to_select_duration": "Umožnit rezervující osobě vybrat dobu trvání",
"impersonate_user_tip": "Všechna použití této funkce jsou kontrolována.",
"impersonating_user_warning": "Zosobnění uživatelského jména „{{user}}“.",
"impersonating_stop_instructions": "<0>Kliknutím zde zastavte</0>.",
@@ -1015,6 +1032,9 @@
"error_removing_app": "Při odstraňování aplikace došlo k chybě",
"web_conference": "Webová konference",
"requires_confirmation": "Vyžaduje potvrzení",
"always_requires_confirmation": "Vždy",
"requires_confirmation_threshold": "Pokud je rezervace provedena s předstihem < {{time}} $t({{unit}}_timeUnit), je nutné potvrzení",
"may_require_confirmation": "Může vyžadovat potvrzení",
"nr_event_type_one": "Typ události: {{count}}",
"nr_event_type_other": "Typy událostí: {{count}}",
"add_action": "Přidat akci",
@@ -1102,6 +1122,9 @@
"event_limit_tab_description": "Jak často lze u vás provést rezervaci",
"event_advanced_tab_description": "Nastavení kalendáře a další...",
"event_advanced_tab_title": "Upřesnit",
"event_setup_multiple_duration_error": "Nastavení události: více dob trvání vyžaduje alespoň 1 možnost.",
"event_setup_multiple_duration_default_error": "Nastavení události: vyberte prosím platnou výchozí dobu trvání.",
"event_setup_booking_limits_error": "Limity rezervací musí být seřazeny vzestupně. [den,týden,měsíc,rok]",
"select_which_cal": "Vyberte kalendář, do kterého se mají přidávat rezervace",
"custom_event_name": "Vlastní název události",
"custom_event_name_description": "Vytvářejte vlastní názvy událostí, které se budou zobrazovat v kalendáři událostí",
@@ -1155,8 +1178,11 @@
"invoices": "Faktury",
"embeds": "Vložené kalendáře",
"impersonation": "Ztotožnění",
"impersonation_description": "Nastavení správy ztotožnění uživatele",
"users": "Uživatelé",
"profile_description": "Správa nastavení profilu {{appName}}",
"users_description": "Tady najdete seznam všech uživatelů",
"users_listing": "Výpis uživatelů",
"general_description": "Správa nastavení jazyka a časového pásma",
"calendars_description": "Konfigurace způsobu interakce typů událostí s kalendáři",
"appearance_description": "Správa nastavení vzhledu rezervace",
@@ -1361,6 +1387,7 @@
"number_sms_notifications": "Číslo telefonu (SMS oznámení)",
"attendee_email_workflow": "E-mail účastníka",
"attendee_email_info": "E-mail osoby provádějící rezervaci",
"kbar_search_placeholder": "Zadejte příkaz nebo vyhledejte...",
"invalid_credential": "Ale ne! Zdá se, že platnost oprávnění vypršela nebo byla zrušena. Proveďte novou instalaci.",
"choose_common_schedule_team_event": "Vyberte společný plán",
"choose_common_schedule_team_event_description": "Tuto možnost povolte, pokud chcete používat společný plán mezi hostiteli. Při zakázání této možnosti bude každý hostitel rezervován na základě svého výchozího plánu.",
@@ -1371,5 +1398,43 @@
"test_preview": "Testovací náhled",
"route_to": "Směrovat na",
"test_preview_description": "Otestujte svůj směrovací formulář bez odeslání jakýchkoli dat",
"test_routing": "Test směrování"
"test_routing": "Test směrování",
"payment_app_disabled": "Správce zakázal platební aplikaci",
"edit_event_type": "Upravit typ události",
"collective_scheduling": "Kolektivní plánování",
"make_it_easy_to_book": "Usnadněte si rezervaci týmu, pokud jsou všichni k dispozici.",
"find_the_best_person": "Najděte nejlepšího dostupného člověka a procházejte tým.",
"fixed_round_robin": "Pevný „round robin“",
"add_one_fixed_attendee": "Přidejte jednoho pevně stanoveného účastníka a „round robin“ přes daný počet účastníků.",
"calcom_is_better_with_team": "Cal.com je s týmy lepší",
"add_your_team_members": "Přidejte členy týmu k typům událostí. Použijte kolektivní plánování, abyste zahrnuli všechny, případně vyhledejte tu nejvhodnější osobu s plánováním „round robin“.",
"booking_limit_reached": "Byl dosažen limit rezervací pro tento typ události",
"admin_has_disabled": "Administrátor zakázal {{appName}}",
"disabled_app_affects_event_type": "Administrátor zakázal {{appName}}, což ovlivňuje váš typ události {{eventType}}",
"disable_payment_app": "Administrátor zakázal {{appName}}, což ovlivňuje váš typ události {{title}}. Účastníci si stále mohou rezervovat tento typ události, ale nebudou vyzváni k platbě. Abyste tomu zabránili, můžete tento typ události skrýt, dokud správce tento způsob platby znovu nepovolí.",
"payment_disabled_still_able_to_book": "Účastníci si stále mohou rezervovat tento typ události, ale nebudou vyzváni k platbě. Abyste tomu zabránili, můžete tento typ události skrýt, dokud správce tento způsob platby znovu nepovolí.",
"app_disabled_with_event_type": "Administrátor zakázal {{appName}}, což ovlivňuje váš typ události {{title}}.",
"app_disabled_video": "Administrátor zakázal {{appName}}, což může ovlivnit vaše typy událostí. Pokud máte typy událostí s {{appName}} jako umístění, bude výchozí nastavení Cal Video.",
"app_disabled_subject": "Aplikace {{appName}} byla zakázána",
"navigate_installed_apps": "Přejděte na nainstalované aplikace",
"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.",
"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",
"disable_app": "Zakázat aplikaci",
"disable_app_description": "Zakázání této aplikace by mohlo způsobit problémy s interakcí uživatelů s aplikací Cal",
"edit_keys": "Upravit klávesy",
"no_available_apps": "Nejsou k dispozici žádné aplikace",
"no_available_apps_description": "Zkontrolujte, zda máte ve svém nasazení v části „packages/app-store“ k dispozici aplikace",
"no_apps": "V této instanci Cal nejsou povoleny žádné aplikace",
"apps_settings": "Nastavení aplikací",
"fill_this_field": "Vyplňte prosím toto pole",
"options": "Možnosti",
"enter_option": "Zadejte možnost {{index}}",
"add_an_option": "Přidat možnost",
"radio": "Přepínač",
"event_type_duplicate_copy_text": "{{slug}} – kopírovat",
"set_as_default": "Nastavit jako výchozí"
}
+113 -11
View File
@@ -7,6 +7,7 @@
"upgrade_now": "Jetzt upgraden",
"accept_invitation": "Einladung annehmen",
"have_any_questions": "Haben Sie Fragen? Wir sind hier um Ihnen zu helfen.",
"reset_password_subject": "{{appName}}: Anleitung zum Zurücksetzen des Passworts",
"event_declined_subject": "Abgelehnt: {{eventType}} mit {{name}} am {{date}}",
"event_cancelled_subject": "Abgesagt: {{eventType}} mit {{name}} am {{date}}",
"event_request_declined": "Ihre Event-Anfrage wurde abgelehnt",
@@ -50,6 +51,7 @@
"still_waiting_for_approval": "Ein Termin wartet noch auf Bestätigung",
"event_is_still_waiting": "Termin-Anfrage wartet: {{attendeeName}} - {{date}} - {{eventType}}",
"no_more_results": "Keine weiteren Ergebnisse",
"no_results": "Keine Ergebnisse",
"load_more_results": "Weitere Ergebnisse laden",
"integration_meeting_id": "{{integrationName}} Termin-ID: {{meetingId}}",
"confirmed_event_type_subject": "Bestätigt: {{eventType}} mit {{name}} am {{date}}",
@@ -97,7 +99,9 @@
"join_team": "Team beitreten",
"manage_this_team": "Dieses Team verwalten",
"team_info": "Informationen zum Team",
"request_another_invitation_email": "Falls Sie {{toEmail}} lieber nicht als Ihre {{appName}}-E-Mail verwenden möchten oder bereits über ein {{appName}}-Konto verfügen, fordern Sie bitte eine weitere Einladung zu dieser E-Mail an.",
"you_have_been_invited": "Sie wurden eingeladen, dem Team {{teamName}} beizutreten",
"user_invited_you": "{{user}} hat Sie eingeladen, dem Team {{team}} auf {{appName}} beizutreten",
"hidden_team_member_title": "Sie sind in diesem Team ausgeblendet",
"hidden_team_member_message": "Ihre Teammitgliedschaft ist nicht bezahlt, entweder upgraden Sie Ihren Account auf Pro oder lassen den Team-Organisator wissen, dass er für Ihren Platz im Team bezahlen kann.",
"hidden_team_owner_message": "Sie benötigen einen Pro-Account um die Teamfunktionen zu verweden. Sie werden dem Team nicht angezeigt, bis der Account upgegradet wurde.",
@@ -109,6 +113,8 @@
"team_upgraded_successfully": "Das Update für Ihr Team war erfolgreich!",
"use_link_to_reset_password": "Benutzen Sie den Link, um Ihr Passwort zurückzusetzen",
"hey_there": "Hallo,",
"forgot_your_password_calcom": "Passwort vergessen? - {{appName}}",
"delete_webhook_confirmation_message": "Sind Sie sicher, dass Sie diesen Webhook löschen möchten? Sie werden keine {{appName}} Meetingdaten mehr unter einer bestimmten URL, in Echtzeit erhalten, wenn ein Ereignis geplant oder abgesagt wird.",
"confirm_delete_webhook": "Ja, Webhook löschen",
"edit_webhook": "Webhook bearbeiten",
"delete_webhook": "Webhook löschen",
@@ -126,9 +132,9 @@
"no_data_yet": "Noch keine Daten",
"ping_test": "Pingtest",
"add_to_homescreen": "Fügen Sie diese App Ihrem Startbildschirm für schnelleren Zugriff hinzu.",
"upcoming": "Bevorstehend",
"recurring": "Wiederkehrend",
"past": "Vergangen",
"upcoming": "Bevorstehende",
"recurring": "Wiederkehrende",
"past": "Vergangene",
"choose_a_file": "Datei auswählen...",
"upload_image": "Bild hochladen",
"upload_target": "{{target}} hochladen",
@@ -184,6 +190,7 @@
"30min_meeting": "30 Minuten Termin",
"secret": "Passwort",
"leave_blank_to_remove_secret": "Leer lassen, um Passwort zu entfernen",
"webhook_secret_key_description": "Stellen Sie sicher, dass Ihr Server aus Sicherheitsgründen nur die erwarteten {{appName}}-Anfragen empfängt",
"secret_meeting": "Geheimer Termin",
"login_instead": "Stattdessen einloggen",
"already_have_an_account": "Haben Sie bereits ein Konto?",
@@ -227,11 +234,13 @@
"details": "Details",
"welcome": "Willkommen",
"welcome_back": "Willkommen zurück",
"welcome_to_calcom": "Willkommen bei {{appName}}",
"welcome_instructions": "Wie ist Ihr Name und in welcher Zeitzone sind Sie? Dies kann später bearbeitet werden.",
"connect_caldav": "Mit CalDav-Server verbinden",
"credentials_stored_and_encrypted": "Ihre Zugangsdaten werden verschlüsselt gespeichert.",
"connect": "Verbinden",
"try_for_free": "Kostenlos testen",
"create_booking_link_with_calcom": "Erstellen Sie Ihren eigenen Buchungslink mit {{appName}}",
"who": "Wer",
"what": "Was",
"when": "Wann",
@@ -239,9 +248,10 @@
"add_to_calendar": "Zum Kalender hinzufügen",
"add_another_calendar": "Einen weiteren Kalender hinzufügen",
"other": "Sonstige",
"email_sign_in_subject": "Ihr Anmelde-Link für {{appName}}",
"emailed_you_and_attendees": "Wir haben Ihnen und den anderen Teilnehmern eine Einladung zum Kalender mit allen Details zugeschickt.",
"emailed_you_and_attendees_recurring": "Wir haben Ihnn und den anderen Teilnemern eine Kalendereinladung für den ersten Termin dieser wiederkehrenden Termins gesendet.",
"emailed_you_and_any_other_attendees": "Wir haben Ihnen und allen anderen Teilnehmer diesen Informationen per E-Mail geschickt.",
"emailed_you_and_attendees_recurring": "Wir haben Ihnen und den anderen Teilnemern eine Kalendereinladung für den ersten Termin dieser wiederkehrenden Termins gesendet.",
"emailed_you_and_any_other_attendees": "Wir haben Ihnen und allen anderen Teilnehmern diese Informationen per E-Mail zugeschickt.",
"needs_to_be_confirmed_or_rejected": "Ihre Buchung muss noch bestätigt oder abgelehnt werden.",
"needs_to_be_confirmed_or_rejected_recurring": "Ihr wiederkehrender Termin muss noch bestätigt oder abgelehnt werden.",
"user_needs_to_confirm_or_reject_booking": "{{user}} muss den Termin noch bestätigen oder ablehnen.",
@@ -252,7 +262,7 @@
"submitted_recurring": "Ihr wiederkehrender Termin wurde gebucht",
"booking_submitted": "Ihre Buchung wurde versandt",
"booking_submitted_recurring": "Ihre wiederkehrende Buchung wurde versandt",
"booking_confirmed": "Ihre Termin wurde bestätigt",
"booking_confirmed": "Ihr Termin wurde bestätigt",
"booking_confirmed_recurring": "Ihr wiederkehrender Termin wurde bestätigt",
"warning_recurring_event_payment": "Zahlungen werden bei wiederkehrenden Terminen noch nicht unterstützt",
"warning_payment_recurring_event": "Zahlungen werden bei wiederkehrenden Terminen noch nicht unterstützt",
@@ -282,7 +292,7 @@
"error_end_time_next_day": "Die Endzeit darf nicht größer als 24 Stunden sein",
"back_to_bookings": "Zurück zu den Buchungen",
"free_to_pick_another_event_type": "Sie können jederzeit einen anderen Termin wählen.",
"cancelled": "Abgesagt",
"cancelled": "Abgesagte",
"cancellation_successful": "Stornierung erfolgreich",
"really_cancel_booking": "Möchten Sie wirklich stornieren?",
"cannot_cancel_booking": "Sie können diese Buchung nicht stornieren",
@@ -303,7 +313,7 @@
"bookings": "Buchungen",
"bookings_description": "Sehen Sie anstehende und vergangene Veranstaltungen, die über Ihre Events gebucht wurden.",
"upcoming_bookings": "Sobald jemand eine Zeit bei Ihnen bucht können Sie das hier sehen.",
"recurring_bookings": "Sobald jemand einen wiederkehrenden Termin mit Ihnen bucht er hier erscheinen.",
"recurring_bookings": "Sobald jemand einen wiederkehrenden Termin mit Ihnen bucht wird er hier erscheinen.",
"past_bookings": "Ihre früheren Buchungen werden hier angezeigt.",
"cancelled_bookings": "Ihre stornierten Buchungen werden hier angezeigt.",
"unconfirmed_bookings": "Ihre unbestätigten Buchungen werden hier angezeigt.",
@@ -337,6 +347,7 @@
"dark": "Dunkel",
"automatically_adjust_theme": "Theme automatisch basierend auf den Einstellungen des Teilnehmers anpassen",
"user_dynamic_booking_disabled": "Einige der Benutzer in der Gruppe haben derzeit dynamische Gruppenbuchungen deaktiviert",
"allow_dynamic_booking_tooltip": "Gruppenbuchungslinks, die dynamisch durch Hinzufügen mehrerer Benutzernamen mit einem '+' erstellt werden können. Beispiel: '{{appName}}/bailey+peer'",
"allow_dynamic_booking": "Ermöglichen Sie es den Teilnehmenden, Sie über dynamische Gruppenbuchungen zu buchen",
"email": "E-Mail",
"email_placeholder": "max@example.com",
@@ -356,6 +367,8 @@
"team_webhooks": "Team-Webhooks",
"create_new_webhook_to_account": "Erstellen Sie einen neuen Webhook in Ihrem Account",
"new_webhook": "Neuer Webhook",
"receive_cal_meeting_data": "Erhalte {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn ein Termin geplant oder abgesagt wird.",
"receive_cal_event_meeting_data": "Erhalte {{appName}} Termindaten unter einer bestimmten URL in Echtzeit, wenn dieser Termin geplant oder abgesagt wird.",
"responsive_fullscreen_iframe": "Responsives Vollbild-iFrame",
"loading": "Wird geladen...",
"deleting": "Lösche...",
@@ -363,6 +376,7 @@
"developer": "Entwickler",
"manage_developer_settings": "Verwalten Sie Ihre Entwicklereinstellungen.",
"iframe_embed": "iFrame-Einbettung",
"embed_calcom": "Der einfachste Weg, {{appName}} auf Ihrer Website einzubinden.",
"integrate_using_embed_or_webhooks": "Integrieren Sie Cal.com in Ihre Website mit unseren Einbettungs-Optionen oder erhalten Sie Buchungsdaten mit Webhooks.",
"schedule_a_meeting": "Termin planen",
"view_and_manage_billing_details": "Ihre Rechnungsdetails ansehen und verwalten",
@@ -407,6 +421,7 @@
"current_incorrect_password": "Aktuelles Passwort ist falsch",
"password_hint_caplow": "Mischung aus Groß- und Kleinbuchstaben",
"password_hint_min": "Mindestens 8 Zeichen lang",
"password_hint_admin_min": "Mindestens 15 Zeichen lang",
"password_hint_num": "Enthält mindestens 1 Zahl",
"invalid_password_hint": "Das Passwort muss mindestens 7 Zeichen lang sein und mindestens eine Nummer beinhalten, sowie eine Mischung aus Groß- und Kleinbuchstaben sein",
"incorrect_password": "Passwort ist falsch",
@@ -439,22 +454,28 @@
"additional_guests": "+ Weitere Gäste",
"your_name": "Ihr Name",
"email_address": "E-Mail Adresse",
"enter_valid_email": "Bitte geben Sie eine gültige Emaildresse ein",
"enter_valid_email": "Bitte geben Sie eine gültige E-Mail-Adresse ein",
"location": "Ort",
"address": "Adresse",
"enter_address": "Geben Sie eine Adresse ein",
"in_person_attendee_address": "In Person (Adresse von Ihnen)",
"yes": "Ja",
"no": "Nein",
"additional_notes": "Zusätzliche Notizen",
"booking_fail": "Termin konnte nicht gebucht werden.",
"reschedule_fail": "Termin konnte nicht neugeplant werden.",
"share_additional_notes": "Bitten teilen Sie Notizen zur Vorbereitung des Termins, falls nötig.",
"share_additional_notes": "Teilen Sie uns weitere Informationen mit, die zur Vorbereitung des Termins hilfreich sind.",
"booking_confirmation": "Bestätigen Sie {{eventTypeTitle}} mit {{profileName}}",
"booking_reschedule_confirmation": "Planen Sie Ihr {{eventTypeTitle}} mit {{profileName}} um",
"in_person_meeting": "Vor-Ort-Termin",
"attendeeInPerson": "Persönlich (Teilnehmeradresse)",
"inPerson": "Persönlich (Organisator-Adresse)",
"link_meeting": "Termin verknüpfen",
"phone_call": "Telefonat",
"your_number": "Ihre Telefonnummer",
"phone_number": "Telefonnummer",
"attendee_phone_number": "Telefonnummer des Teilnehmers",
"organizer_phone_number": "Telefonnummer des Organisators",
"host_phone_number": "Ihre Telefonnummer",
"enter_phone_number": "Telefonnummer eingeben",
"reschedule": "Neuplanen",
@@ -494,6 +515,8 @@
"invite_new_member_description": "Hinweis: Dies wird <1>einen zusätzlichen Platz(15 $/m)</1> in Ihrem Abonnement kosten.",
"invite_new_team_member": "Laden Sie jemanden in Ihr Team ein.",
"change_member_role": "Rolle des Teammitglieds ändern",
"disable_cal_branding": "{{appName}}-Branding deaktivieren",
"disable_cal_branding_description": "Blendet alle {{appName}}-Werbung auf Ihren öffentlichen Seiten aus.",
"danger_zone": "Achtung",
"back": "Zurück",
"cancel": "Abbrechen",
@@ -539,6 +562,10 @@
"collective": "Kollektiv",
"collective_description": "Planen Sie Meetings, bei denen alle ausgewählten Teammitglieder verfügbar sind.",
"duration": "Dauer",
"available_durations": "Verfügbare Laufzeiten",
"default_duration": "Standardlaufzeit",
"default_duration_no_options": "Bitte wählen Sie zuerst die verfügbaren Laufzeiten aus",
"multiple_duration_mins": "{{count}} $t(minute_timeUnit)",
"minutes": "Minuten",
"round_robin": "Round Robin",
"round_robin_description": "Treffen zwischen mehreren Teammitgliedern durchwechseln.",
@@ -610,6 +637,7 @@
"changed_team_billing_info": "Ab Januar 2022 berechnen wir pro Sitzplatz für Teammitglieder. Mitglieder deines Teams, die Pro kostenlos hatten, sind jetzt in einer 14 Tage Testphase. Sobald die Testversion abläuft, werden diese Mitglieder vor Ihrem Team verborgen, es sei denn, Sie upgraden jetzt.",
"create_manage_teams_collaborative": "Erstellen und verwalten Sie Teams, um kollaborative Funktionen zu nutzen.",
"only_available_on_pro_plan": "Diese Funktion ist nur im Pro Plan verfügbar",
"remove_cal_branding_description": "Um das {{appName}}-Branding von Ihren Buchungsseiten zu entfernen, müssen Sie auf ein Pro-Konto upgraden.",
"edit_profile_info_description": "Bearbeiten Sie Ihre Profilinformationen, welche bei Ihrem Cal-Link angezeigt werden.",
"change_email_tip": "Möglicherweise müssen Sie sich aus- und einloggen, um die Änderung wirksam zu machen.",
"little_something_about": "Ein wenig über sich selbst.",
@@ -658,19 +686,28 @@
"private_link_label": "Privater Link",
"private_link_hint": "Ihr privater Link wird nach jeder Nutzung neu generiert",
"copy_private_link": "Privaten Link kopieren",
"private_link_description": "Erzeugen Sie einen privaten Link, den Sie teilen können, ohne Ihren {{appName}}-Benutzername preiszugeben",
"invitees_can_schedule": "Teilnehmende können folgendes buchen",
"date_range": "Datumszeitraum",
"calendar_days": "Kalendertage",
"business_days": "Werktage",
"set_address_place": "Adresse oder Ort festlegen",
"set_link_meeting": "Einen Link zum Termin setzen",
"cal_invitee_phone_number_scheduling": "{{appName}} wird Ihren Teilnehmer bitten, vor der Planung eine Telefonnummer einzugeben.",
"cal_provide_google_meet_location": "{{appName}} wird einen Google Meet Ort zur Verfügung stellen.",
"cal_provide_zoom_meeting_url": "{{appName}} wird eine Zoom Meeting URL zur Verfügung stellen.",
"cal_provide_tandem_meeting_url": "{{appName}} wird eine Tandem Meeting URL zur Verfügung stellen.",
"cal_provide_video_meeting_url": "{{appName}} wird eine Video-Meeting URL zur Verfügung stellen.",
"cal_provide_jitsi_meeting_url": "Cal stellt eine Jitsi Meet URL zur Verfügung.",
"cal_provide_huddle01_meeting_url": "{{appName}} wird eine Huddle01 web3 Video-Meeting URL zur Verfügung stellen.",
"cal_provide_teams_meeting_url": "{{appName}} wird eine MS Teams Meeting-URL zur Verfügung stellen. HINWEIS: SIE BRAUCHEN EIN BERUFS- ODER SCHULKONTO",
"require_payment": "Zahlung erforderlich",
"commission_per_transaction": "Provision pro Transaktion",
"event_type_updated_successfully_description": "Ihr Termintyp wurde erfolgreich aktualisiert.",
"hide_event_type": "Termintyp ausblenden",
"edit_location": "Standort bearbeiten",
"into_the_future": "in die Zukunft",
"when_booked_with_less_than_notice": "Bei Buchung mit weniger als <time></time> Vorlauf",
"within_date_range": "Innerhalb eines Zeitraums",
"indefinitely_into_future": "Auf unbestimmte Zeit in die Zukunft",
"add_new_custom_input_field": "Neues individuelles Eingabefeld hinzufügen",
@@ -687,6 +724,7 @@
"confirm_delete_event_type": "Ja, Termintyp löschen",
"delete_account": "Account löschen",
"confirm_delete_account": "Ja, Account löschen",
"delete_account_confirmation_message": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten? Sie können dann nicht mehr über Ihre geteilten Links gebucht werden und alle Einstellungen werden gelöscht.",
"integrations": "Integrationen",
"apps": "Apps",
"apps_description": "Hier findest du eine Auflistung deiner Apps",
@@ -714,6 +752,7 @@
"toggle_calendars_conflict": "Wechseln Sie die Kalender in denen nach Konflikten gesucht werden sollen, um Doppelbuchungen zu vermeiden.",
"select_destination_calendar": "Erstelle Termine im",
"connect_additional_calendar": "Zusätzlichen Kalender verbinden",
"calendar_updated_successfully": "Kalender erfolgreich aktualisiert",
"conferencing": "Videokonferenzen",
"calendar": "Kalender",
"payments": "Zahlungen",
@@ -789,6 +828,7 @@
"enterprise_license_description": "Um diese Funktion zu aktivieren, holen Sie sich einen Deployment-Schlüssel von der {{consoleUrl}}-Konsole und fügen Sie ihn als CALCOM_LICENSE_KEY zu Ihrer .env hinzu. Wenn Ihr Team bereits eine Lizenz hat, wenden Sie sich bitte an {{supportMail}} für Hilfe.",
"missing_license": "Lizenz fehlt",
"signup_requires": "Kommerzielle Lizenz erforderlich",
"signup_requires_description": "{{companyName}} bietet zur Zeit keine kostenlose Open-Source-Version der Anmeldeseite an. Um vollen Zugriff auf die Registrierungskomponenten zu erhalten, müssen Sie eine kommerzielle Lizenz erwerben. Für den persönlichen Gebrauch empfehlen wir die Prisma Data Platform oder jede andere Postgres-Schnittstelle zur Erstellung von Accounts.",
"next_steps": "Nächste Schritte",
"acquire_commercial_license": "Eine kommerzielle Lizenz erwerben",
"the_infrastructure_plan": "Der Infrastrukturplan ist nutzungsbasiert und bietet Startup-freundliche Discounts an.",
@@ -877,6 +917,7 @@
"user_impersonation_heading": "Benutzeridentitätswechsel",
"user_impersonation_description": "Erlaubt es unserem Support-Team, sich vorübergehend mit Ihrem Konto anzumelden, um jegliche Probleme, die Sie gemeldet haben, schneller zu lösen.",
"team_impersonation_description": "Ermöglicht den Team-Administratoren, sich vorübergehend als Sie einzuloggen.",
"allow_booker_to_select_duration": "Bucher erlauben, die Länge zu wählen",
"impersonate_user_tip": "Alle Verwendungen dieser Funktion werden geprüft.",
"impersonating_user_warning": "Imitiere den Account {{user}}.",
"impersonating_stop_instructions": "<0>Klicke hier um zu stoppen </0>.",
@@ -890,11 +931,15 @@
"update_location": "Ort aktualisieren",
"location_updated": "Ort aktualisiert",
"email_validation_error": "Das sieht nicht wie eine E-Mail-Adresse aus",
"place_where_cal_widget_appear": "Kopieren Sie diesen Code an die Stelle in Ihrem HTML, an der Ihr {{appName}}-Widget erscheinen soll.",
"create_update_react_component": "Erstellen oder Aktualisieren einer existierenden Reaktionskomponente wie unten gezeigt.",
"copy_code": "Code kopieren",
"code_copied": "Code kopiert!",
"how_you_want_add_cal_site": "Wie möchten Sie {{appName}} zu Ihrer Website hinzufügen?",
"choose_ways_put_cal_site": "Wählen Sie eine der folgenden Möglichkeiten, um {{appName}} in Ihre Website zu integrieren.",
"setting_up_zapier": "Einrichtung Ihrer Zapier-Integration",
"generate_api_key": "Api Key generieren",
"generate_api_key_description": "Generieren Sie einen API-Schlüssel für {{appName}} unter",
"your_unique_api_key": "Ihr eindeutiger API-Key",
"copy_safe_api_key": "Kopieren Sie diesen API-Schlüssel und speichern Sie ihn an einem sicheren Ort. Wenn Sie diesen Schlüssel verlieren, müssen Sie einen neuen generieren.",
"zapier_setup_instructions": "<0>Gehen Sie zu: <1>Zapier-Einladungs-Link</1></0><1>Logge Sie sich in Ihr Zapier-Konto ein und erstelle einen neuen Zap.</1><2>Wählen Sie Cal.com als ihre Trigger-App aus. Wählen Sie auch ein Auslöserereignis.</2><3>Wähle Sie ihr Konto aus und geben dann ihren eindeutigen API-Key ein.</3><4>Teste Sie den Trigger.</4><5>Alles fertig!</5>",
@@ -902,6 +947,7 @@
"connect_apple_server": "Mit Apple-Server verbinden",
"connect_caldav_server": "Mit CalDav-Server verbinden",
"calendar_url": "Kalender-URL",
"apple_server_generate_password": "Generieren Sie ein App-spezifisches Passwort für {{appName}} unter",
"credentials_stored_encrypted": "Ihre Zugangsdaten werden verschlüsselt gespeichert.",
"it_stored_encrypted": "Er wird gespeichert und verschlüsselt.",
"go_to_app_store": "Zum App Store",
@@ -989,6 +1035,9 @@
"error_removing_app": "Fehler beim Entfernen der App",
"web_conference": "Webkonferenz",
"requires_confirmation": "Erfordert Bestätigung",
"always_requires_confirmation": "Immer",
"requires_confirmation_threshold": "Erfordert eine Bestätigung bei Buchung mit < {{time}} $t({{unit}}_timeUnit) Vorlauf",
"may_require_confirmation": "Kann Bestätigung erfordern",
"nr_event_type_one": "{{count}} Ereignistyp",
"nr_event_type_other": "{{count}} Ereignistypen",
"add_action": "Aktion hinzufügen",
@@ -1076,6 +1125,9 @@
"event_limit_tab_description": "Wie oft können Sie gebucht werden",
"event_advanced_tab_description": "Kalendereinstellungen & mehr...",
"event_advanced_tab_title": "Erweitert",
"event_setup_multiple_duration_error": "Terminkonfiguration: Mehrere Laufzeiten erfordern mindestens eine Option.",
"event_setup_multiple_duration_default_error": "Termineinrichtung: Bitte wählen Sie eine gültige Standarddauer.",
"event_setup_booking_limits_error": "Die Buchungslimits müssen in aufsteigender Reihenfolge sein. [Tag, Woche, Monat, Jahr]",
"select_which_cal": "Wählen Sie den Kalender aus, zu dem Termine hinzugefügt werden sollen",
"custom_event_name": "Eigener Terminname",
"custom_event_name_description": "Erstellen Sie benutzerdefinierte Terminnamen, die im Kalendereintrag angezeigt werden sollen",
@@ -1089,8 +1141,10 @@
"turn_off": "Abschalten",
"settings_updated_successfully": "Einstellungen erfolgreich aktualisiert",
"error_updating_settings": "Fehler beim Aktualisieren der Einstellungen",
"personal_cal_url": "Meine persönliche {{appName}}-URL",
"bio_hint": "Schreiben Sie eine kurze Beschreibung, welche auf Ihrer persönlichen Profil-Seite erscheinen wird.",
"delete_account_modal_title": "Account löschen",
"confirm_delete_account_modal": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten?",
"delete_my_account": "Meinen Account löschen",
"start_of_week": "Wochenstart",
"select_calendars": "Wählen Sie aus, in welchen Kalendern Sie nach Konflikten suchen wollen, um Doppelbuchungen zu vermeiden.",
@@ -1100,6 +1154,7 @@
"custom_brand_colors": "Eigene Marken-Farben",
"customize_your_brand_colors": "Passen Sie Ihre eigene Markenfarbe auf Ihrer Buchungsseite an.",
"pro": "Pro",
"removes_cal_branding": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}.“",
"profile_picture": "Profilbild",
"upload": "Upload",
"add_profile_photo": "Profilbild hinzufügen",
@@ -1168,6 +1223,7 @@
"create_your_first_form": "Erstellen Sie Ihr erstes Formular",
"create_your_first_form_description": "Mithilfe von Leitungsformularen können Sie qualifizierende Fragen stellen und zur richtigen Person oder zum richtigen Veranstaltungstyp weiterleiten.",
"create_your_first_webhook": "Erstellen Sie Ihren ersten Webhook",
"create_your_first_webhook_description": "Mit Webhooks können Sie Meetingdaten in Echtzeit erhalten, sobald etwas in {{appName}} passiert.",
"for_a_maximum_of": "Für maximal",
"event_one": "Ereignis",
"event_other": "Ereignisse",
@@ -1183,6 +1239,7 @@
"workflow_example_4": "Eine Erinnerung per SMS an die Teilnehmer verschicken, 24 Stunden bevor das Event startet",
"workflow_example_5": "Benutzerdefinierte E-Mail senden, wenn der Termin zum Moderieren neu geplant wird",
"workflow_example_6": "Benutzerdefinierte SMS senden, wenn der Termin zum Moderieren gebucht wird",
"welcome_to_cal_header": "Willkommen bei {{appName}}!",
"edit_form_later_subtitle": "Sie können dies später bearbeiten.",
"connect_calendar_later": "Ich verbinde meinen Kalender später",
"set_my_availability_later": "Ich werde meine Verfügbarkeit später festlegen",
@@ -1192,6 +1249,7 @@
"booking_appearance": "Darstellung der Buchungsseite",
"appearance_team_description": "Einstellungen für die Buchungsdarstellung Ihres Teams verwalten",
"only_owner_change": "Nur der Besitzer dieses Teams kann Änderungen an den Buchungen des Teams vornehmen ",
"team_disable_cal_branding_description": "Entfernt alle {{appName}}-bezogenen Markenzeichen, z. B. „Powered by {{appName}}“",
"invited_by_team": "{{teamName}} hat Sie eingeladen, dem Team als {{role}} beizutreten",
"token_invalid_expired": "Token ist entweder ungültig oder abgelaufen.",
"exchange_add": "Mit Microsoft Exchange verbinden",
@@ -1211,11 +1269,13 @@
"theme_dark": "Dunkel",
"theme_system": "Systemstandard",
"add_a_team": "Team hinzufügen",
"add_webhook_description": "Erhalten Sie Meetingdaten in Echtzeit, wenn etwas in {{appName}} passiert",
"triggers_when": "Löst aus, wenn",
"test_webhook": "Bitte führen Sie einen Pingtest vor der Erstellung durch.",
"enable_webhook": "Webhook aktivieren",
"add_webhook": "Webhook hinzufügen",
"webhook_edited_successfully": "Webhook gespeichert",
"webhooks_description": "Erhalten Sie Meetingdaten in Echtzeit, wenn etwas in {{appName}} passiert",
"api_keys_description": "Generieren Sie API-Schlüssel für den Zugriff auf Ihren eigenen Account",
"new_api_key": "Neuer API-Schlüssel",
"active": "Aktiv",
@@ -1224,6 +1284,7 @@
"embeds_title": "HTML iframe-Einbettung",
"embeds_description": "Alle Event-Typen auf Ihrer Website einbetten",
"create_first_api_key": "Erstellen Sie Ihren ersten API-Schlüssel",
"create_first_api_key_description": "API-Schlüssel erlauben es anderen Apps, mit {{appName}} zu kommunizieren",
"back_to_signin": "Zurück zur Anmeldung",
"reset_link_sent": "Zurücksetzungslink gesendet",
"password_reset_email": "Eine E-Mail mit Anweisungen zum Zurücksetzen Ihres Passworts ist auf dem Weg zu {{email}}.",
@@ -1231,12 +1292,14 @@
"password_updated": "Passwort aktualisiert!",
"pending_payment": "Zahlung ausstehend",
"confirmation_page_rainbow": "Sperren Sie Ihr Ereignis hinter Tokens oder NFTs auf Ethereum, Polygon und mehr.",
"not_on_cal": "Nicht auf {{appName}}",
"no_calendar_installed": "Kein Kalender installiert",
"no_calendar_installed_description": "Sie haben noch keinen Ihrer Kalender verbunden",
"add_a_calendar": "Kalender hinzufügen",
"change_email_hint": "Möglicherweise müssen Sie sich aus- und einloggen, um jegliche Änderung wirksam zu machen",
"confirm_password_change_email": "Bitte bestätigen Sie Ihr Passwort, bevor Sie Ihre E-Mail-Adresse ändern",
"seats": "Plätze",
"every_app_published": "Jede App, die im {{appName}} App Store veröffentlicht wird, ist Open Source und gründlich durch Peer Reviews getestet. {{companyName}} unterstützt oder zertifiziert diese Apps jedoch nicht, es sei denn, sie werden von {{appName}} selbst veröffentlicht. Wenn Sie auf unangemessene Inhalte oder Verhaltensweisen stoßen, melden Sie diese bitte.",
"report_app": "App melden",
"limit_booking_frequency": "Buchungsfrequenz begrenzen",
"limit_booking_frequency_description": "Begrenzen, wie oft dieses Ereignis gebucht werden kann",
@@ -1338,5 +1401,44 @@
"test_preview": "Vorschau testen",
"route_to": "Weiterleiten zu",
"test_preview_description": "Testen Sie Ihr Weiterleitungsformular, ohne Daten zu senden",
"test_routing": "Testweiterleitung"
"test_routing": "Testweiterleitung",
"payment_app_disabled": "Ein Administrator hat eine Zahlungs-App deaktiviert",
"edit_event_type": "Termintyp bearbeiten",
"collective_scheduling": "Kollektive Terminplanung",
"make_it_easy_to_book": "Machen Sie es einfach, Ihr Team zu buchen, wenn jeder verfügbar ist.",
"find_the_best_person": "Finden Sie die beste verfügbare Person und rotieren Sie durch Ihr Team.",
"fixed_round_robin": "Festgelegter Round Robin",
"add_one_fixed_attendee": "Fügen Sie einen festen Teilnehmer und einen Round Robin mit einer Reihe von Teilnehmern hinzu.",
"calcom_is_better_with_team": "Cal.com ist mit Teams besser",
"add_your_team_members": "Fügen Sie Ihre Teammitglieder zu Ihren Termintypen hinzu. Nutzen Sie kollektive Planung, um alle einzubinden oder finden Sie die geeignetste Person mit der Round Robin Planung.",
"booking_limit_reached": "Das Buchungslimit für diesen Termintyp wurde erreicht",
"admin_has_disabled": "Ein Admin hat {{appName}} deaktiviert",
"disabled_app_affects_event_type": "Ein Admin hat {{appName}} deaktiviert, was Ihren Termintyp {{eventType}} betrifft",
"disable_payment_app": "Der Admin hat {{appName}} deaktiviert, wodurch Ihr Termintyp {{title}} beeinflusst wurde. Die Teilnehmer können diesen Termintyp immer noch buchen, werden aber nicht zur Zahlung aufgefordert. Sie können den Termintyp ausblenden, um dies zu verhindern, bis Ihr Administrator Ihre Zahlungsmethode wieder aktiviert.",
"payment_disabled_still_able_to_book": "Die Teilnehmer können diesen Termintyp immer noch buchen, werden aber nicht zur Zahlung aufgefordert. Sie können den Termintyp ausblenden, um dies zu verhindern, bis Ihr Administrator Ihre Zahlungsmethode wieder aktiviert.",
"app_disabled_with_event_type": "Der Admin hat {{appName}} deaktiviert, was Ihren Termintyp {{title}} betrifft.",
"app_disabled_video": "Der Administrator hat {{appName}} deaktiviert, was Ihre Termintypen beeinflussen kann. Wenn Sie Termintypen mit {{appName}} als Ort haben, wird es standardmäßig zu Cal Video geändert.\n",
"app_disabled_subject": "{{appName}} wurde deaktiviert",
"navigate_installed_apps": "Zu installierte Apps wechseln",
"disabled_calendar": "Wenn Sie einen anderen Kalender installiert haben, werden neue Buchungen zu dem Kalender hinzugefügt. Verbinden Sie andernfalls einen neuen Kalender, damit Sie keine neuen Buchungen verpassen.",
"enable_apps": "Apps aktivieren",
"enable_apps_description": "Apps aktivieren, die Benutzer mit Cal.com integrieren können",
"app_is_enabled": "{{appName}} ist aktiviert",
"app_is_disabled": "{{appName}} ist deaktiviert",
"keys_have_been_saved": "Schlüssel wurden gespeichert",
"disable_app": "App deaktivieren",
"disable_app_description": "Das Deaktivieren dieser App kann Probleme mit den Interaktionen Ihrer Nutzer mit Cal verursachen",
"edit_keys": "Schlüssel bearbeiten",
"admin_apps_description": "Hier findest du eine Auflistung deiner Apps",
"no_available_apps": "Es gibt keine verfügbaren Apps",
"no_available_apps_description": "Bitte stellen Sie sicher, dass sich unter „packages/app-store“ Apps in Ihrem Deployment befinden",
"no_apps": "In dieser Instanz von Cal sind keine Apps aktiviert",
"apps_settings": "Apps-Einstellungen",
"fill_this_field": "Bitte füllen Sie dieses Feld aus",
"options": "Optionen",
"enter_option": "Option {{index}} eingeben",
"add_an_option": "Option hinzufügen",
"radio": "Radio",
"event_type_duplicate_copy_text": "{{slug}}-Kopie",
"set_as_default": "Als Standard festlegen"
}
@@ -0,0 +1,111 @@
{
"day_one": "{{count}} ημέρα",
"day_other": "{{count}} ημέρες",
"second_one": "{{count}} δευτερόλεπτο",
"second_other": "{{count}} δευτερόλεπτα",
"upgrade_now": "Αναβαθμίστε τώρα",
"accept_invitation": "Αποδοχή Πρόσκλησης",
"have_any_questions": "Έχετε ερωτήσεις? Είμαστε εδώ για να βοηθήσουμε.",
"reset_password_subject": "{{appName}}: Οδηγίες επαναφοράς κωδικού πρόσβασης",
"event_declined_subject": "Απορρίφθηκε: {{eventType}} με {{name}} στις {{date}}",
"event_cancelled_subject": "Ακύρωση: {{eventType}} με {{name}} στις {{date}}",
"need_to_reschedule_or_cancel": "Χρειάζεται να επαναπρογραμματίσετε ή να ακυρώσετε;",
"cancellation_reason": "Λόγος ακύρωσης (προαιρετικό)",
"rejection_reason": "Λόγος απόρριψης",
"rejection_reason_title": "Απόρριψη του αιτήματος κράτησης;",
"rejection_confirmation": "Απόρριψη κράτησης",
"error_message": "Το μήνυμα σφάλματος ήταν: '{{errorMessage}}'",
"refund_failed_subject": "Η επιστροφή χρημάτων απέτυχε: {{name}} - {{date}} - {{eventType}}",
"refund_failed": "Η επιστροφή χρημάτων για την εκδήλωση {{eventType}} με {{userName}} στις {{date}} απέτυχε.",
"a_refund_failed": "Αποτυχία επιστροφής χρημάτων",
"awaiting_payment_subject": "Σε αναμονή Πληρωμής: {{eventType}} με {{name}} στις {{date}}",
"meeting_awaiting_payment": "Η πληρωμή της συνάντησής σας εκκρεμεί",
"help": "Βοήθεια",
"price": "Τιμή",
"payment": "Πληρωμή",
"missing_card_fields": "Λείπουν τα πεδία της κάρτας",
"pay_now": "Πληρωμή τώρα",
"terms_summary": "Περίληψη όρων",
"open_env": "Ανοίξτε το .env και συμφωνήστε με την Άδεια χρήσης μας",
"env_changed": "Έχω αλλάξει το .env μου",
"accept_license": "Αποδοχή Άδειας",
"no_more_results": "Δεν υπάρχουν άλλα αποτελέσματα",
"no_results": "Δεν υπάρχουν αποτελέσματα",
"load_more_results": "Φόρτωση περισσότερων αποτελεσμάτων",
"integration_meeting_id": "ID συνάντησης {{integrationName}} : {{meetingId}}",
"confirmed_event_type_subject": "Επιβεβαίωση: {{eventType}} με {{name}} στις {{date}}",
"new_event_request": "Νέο αίτημα εκδήλωσης: {{attendeeName}} - {{date}} - {{eventType}}",
"confirm_or_reject_request": "Επιβεβαιώστε ή απορρίψτε το αίτημα",
"check_bookings_page_to_confirm_or_reject": "Ελέγξτε τη σελίδα κρατήσεων για να επιβεβαιώσετε ή να απορρίψετε την κράτηση.",
"event_awaiting_approval": "Ένα γεγονός περιμένει την έγκρισή σας",
"event_type": "Τύπος Συμβάντος",
"meeting_password": "Συνθηματικό Συνάντησης",
"meeting_url": "URL Συνάντησης",
"meeting_request_rejected": "Το αίτημα συνάντησής σας απορρίφθηκε",
"hi": "Γεια",
"manage_this_team": "Διαχείριση αυτής της ομάδας",
"team_info": "Πληροφορίες Ομάδας",
"hidden_team_member_title": "Είστε κρυμμένοι σε αυτήν την ομάδα",
"edit_webhook": "Επεξεργασία Webhook",
"delete_webhook": "Διαγραφή Webhook",
"webhook_enabled": "Ενεργοποιημένο Webhook",
"webhook_disabled": "Απενεργοποιημένο Webhook",
"webhook_response": "Απάντηση Webhook",
"webhook_test": "Έλεγχος Webhook",
"webhook_created_successfully": "Το Webhook δημιουργήθηκε επιτυχώς!",
"webhook_updated_successfully": "Το Webhook ενημερώθηκε επιτυχώς!",
"webhook_removed_successfully": "Το Webhook αφαιρέθηκε επιτυχώς!",
"dismiss": "Παράβλεψη",
"no_data_yet": "Δεν υπάρχουν δεδομένα ακόμη",
"upcoming": "Επερχόμενα",
"recurring": "Επαναλαμβανόμενα",
"past": "Παρελθοντικά",
"choose_a_file": "Επιλογή αρχείου...",
"upload_image": "Μεταφόρτωση εικόνας",
"username": "Όνομα χρήστη",
"is_still_available": "είναι ακόμα διαθέσιμο.",
"documentation": "Τεκμηρίωση",
"blog": "Ιστολόγιο",
"blog_description": "Διαβάστε τα τελευταία μας νέα και άρθρα",
"popular_pages": "Δημοφιλείς σελίδες",
"register_now": "Εγγραφή τώρα",
"register": "Εγγραφή",
"page_doesnt_exist": "Η σελίδα δεν υπάρχει.",
"check_spelling_mistakes_or_go_back": "Έλεγχος για ορθογραφικά λάθη ή επιστροφή στην προηγούμενη σελίδα.",
"404_page_not_found": "404: Η σελίδα δεν βρέθηκε.",
"getting_started": "Ξεκινήστε",
"already_have_an_account": "Έχετε ήδη λογαριασμό;",
"create_account": "Δημιουργία Λογαριασμού",
"confirm_password": "Επιβεβαίωση κωδικού πρόσβασης",
"create_your_account": "Δημιουργία λογαριασμού",
"sign_up": "Εγγραφή",
"youve_been_logged_out": "Έχετε αποσυνδεθεί",
"hope_to_see_you_soon": "Ελπίζουμε να σας ξαναδούμε σύντομα!",
"please_try_again_and_contact_us": "Παρακαλούμε δοκιμάστε ξανά και επικοινωνήστε μαζί μας αν το πρόβλημα παραμένει.",
"no_account_exists": "Δεν υπάρχει λογαριασμός που να ταιριάζει με τη διεύθυνση email.",
"2fa_enter_six_digit_code": "Εισάγετε τον εξαψήφιο κωδικό από την εφαρμογή ελέγχου ταυτότητας παρακάτω.",
"create_an_account": "Δημιουργία λογαριασμού",
"dont_have_an_account": "Δεν έχετε λογαριασμό;",
"sign_in_account": "Συνδεθείτε στο λογαριασμό σας",
"sign_in": "Είσοδος",
"connect": "Σύνδεση",
"try_for_free": "Δοκιμάστε δωρεάν",
"add_to_calendar": "Προσθήκη στο ημερολόγιο",
"add_another_calendar": "Προσθήκη άλλου ημερολογίου",
"meeting_is_scheduled": "Η συνάντηση έχει προγραμματιστεί",
"submitted": "Η κράτησή σας έχει υποβληθεί",
"reset_password": "Επαναφορά Κωδικού Πρόσβασης",
"change_your_password": "Αλλαγή κωδικού πρόσβασης",
"show_password": "Εμφάνιση κωδικού πρόσβασης",
"hide_password": "Απόκρυψη κωδικού πρόσβασης",
"try_again": "Δοκιμάστε ξανά",
"sunday_time_error": "Μη έγκυρη ώρα την Κυριακή",
"monday_time_error": "Μη έγκυρη ώρα τη Δευτέρα",
"tuesday_time_error": "Μη έγκυρη ώρα την Τρίτη",
"wednesday_time_error": "Μη έγκυρη ώρα την Τετάρτη",
"thursday_time_error": "Μη έγκυρη ώρα την Πέμπτη",
"friday_time_error": "Μη έγκυρη ώρα την Παρασκευή",
"saturday_time_error": "Μη έγκυρη ώρα το Σάββατο",
"no_meeting_found": "Δε βρέθηκε συνάντηση",
"bookings": "Κρατήσεις"
}
+59 -12
View File
@@ -6,7 +6,7 @@
"second_other": "{{count}} seconds",
"upgrade_now": "Upgrade now",
"accept_invitation": "Accept Invitation",
"calcom_explained": "{{appName}} is the open source Calendly alternative putting you in control of your own data, workflow and appearance.",
"calcom_explained": "{{appName}} provides scheduling infrastructure for absolutely everyone.",
"have_any_questions": "Have questions? We're here to help.",
"reset_password_subject": "{{appName}}: Reset password instructions",
"event_declined_subject": "Declined: {{eventType}} with {{name}} at {{date}}",
@@ -303,6 +303,7 @@
"cancelling_all_recurring": "These are all remaining instances in the recurring event.",
"error_with_status_code_occured": "An error with status code {{status}} occurred.",
"booking_already_cancelled": "This booking was already cancelled",
"booking_already_accepted_rejected": "This booking was already accepted or rejected",
"go_back_home": "Go back home",
"or_go_back_home": "Or go back home",
"no_availability": "Unavailable",
@@ -385,6 +386,8 @@
"go_to_billing_portal": "Go to the billing portal",
"need_anything_else": "Need anything else?",
"further_billing_help": "If you need any further help with billing, our support team are here to help.",
"contact": "Contact",
"our_support_team": "our support team",
"contact_our_support_team": "Contact our support team",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "This user hasn't set up any event types yet.",
@@ -457,6 +460,9 @@
"email_address": "Email address",
"enter_valid_email": "Please enter a valid email",
"location": "Location",
"address": "Address",
"enter_address": "Enter address",
"in_person_attendee_address": "In Person (Attendee Address)",
"yes": "yes",
"no": "no",
"additional_notes": "Additional notes",
@@ -750,7 +756,7 @@
"toggle_calendars_conflict": "Toggle the calendars you want to check for conflicts to prevent double bookings.",
"select_destination_calendar": "Create events on",
"connect_additional_calendar": "Connect additional calendar",
"calendar_updated_successfully":"Calendar updated successfully",
"calendar_updated_successfully": "Calendar updated successfully",
"conferencing": "Conferencing",
"calendar": "Calendar",
"payments": "Payments",
@@ -791,7 +797,7 @@
"no_category_apps_description_analytics": "Add an analytics app for your booking pages",
"no_category_apps_description_automation": "Add an automation app to use",
"no_category_apps_description_other": "Add any other type of app to do all sorts of things",
"installed_app_calendar_description": "Set the calendar(s) to check for conflicts to prevent double bookings.",
"installed_app_calendar_description": "Set the calendars to check for conflicts to prevent double bookings.",
"installed_app_conferencing_description": "Add your favourite video conferencing apps for your meetings",
"installed_app_payment_description": "Configure which payment processing services to use when charging your clients.",
"installed_app_analytics_description": "Configure which analytics apps to use for your booking pages",
@@ -918,7 +924,7 @@
"allow_booker_to_select_duration": "Allow booker to select duration",
"impersonate_user_tip": "All uses of this feature is audited.",
"impersonating_user_warning": "Impersonating username \"{{user}}\".",
"impersonating_stop_instructions": "<0>Click Here to stop</0>.",
"impersonating_stop_instructions": "Click here to stop",
"event_location_changed": "Updated - Your event changed the location",
"location_changed_event_type_subject": "Location Changed: {{eventType}} with {{name}} at {{date}}",
"current_location": "Current Location",
@@ -1088,6 +1094,10 @@
"navigate": "Navigate",
"open": "Open",
"close": "Close",
"upgrade": "Upgrade",
"upgrade_to_access_recordings_title": "Upgrade to access recordings",
"upgrade_to_access_recordings_description":"Recordings are only available as part of our teams plan. Upgrade to start recording your calls",
"recordings_are_part_of_the_teams_plan":"Recordings are part of the teams plan",
"team_feature_teams": "This is a Team feature. Upgrade to Team to see your team's availability.",
"team_feature_workflows": "This is a Team feature. Upgrade to Team to automate your event notifications and reminders with Workflows.",
"show_eventtype_on_profile": "Show on Profile",
@@ -1145,8 +1155,11 @@
"confirm_delete_account_modal": "Are you sure you want to delete your {{appName}} account?",
"delete_my_account": "Delete my account",
"start_of_week": "Start of week",
"recordings_title": "Recordings",
"recording": "Recording",
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
"check_for_conflicts": "Check for conflicts",
"view_recordings": "View recordings",
"adding_events_to": "Adding events to",
"follow_system_preferences": "Follow system preferences",
"custom_brand_colors": "Custom brand colors",
@@ -1213,11 +1226,12 @@
"event_time_info": "The event start time",
"location_info": "The event location",
"organizer_name_info": "Your name",
"additional_notes_info": "The Additional notes of booking",
"additional_notes_info": "The additional notes of booking",
"attendee_name_info": "The person booking's name",
"to": "To",
"workflow_turned_on_successfully": "{{workflowName}} workflow turned {{offOn}} successfully",
"download_responses": "Download Responses",
"download": "Download",
"create_your_first_form": "Create your first form",
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
"create_your_first_webhook": "Create your first Webhook",
@@ -1326,6 +1340,8 @@
"booking_confirmation_success": "Booking confirmation succeeded",
"booking_rejection_success": "Booking rejection succeeded",
"booking_confirmation_fail": "Booking confirmation failed",
"booking_tentative": "This booking is tentative",
"booking_accept_intent": "Oops, I want to accept",
"we_wont_show_again": "We won't show this again",
"couldnt_update_timezone": "We couldn't update the timezone",
"updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}",
@@ -1389,10 +1405,8 @@
"attendee_email_workflow": "Attendee email",
"attendee_email_info": "The person booking's email",
"kbar_search_placeholder": "Type a command or search...",
"free_to_use_apps": "Free",
"enterprise_license": "This is an enterprise feature",
"enterprise_license_description": "To enable this feature, get a deployment key at {{consoleUrl}} console and add it to your .env as CALCOM_LICENSE_KEY. If your team already has a license, please contact {{supportMail}} for help.",
"invalid_credential": "Oh no! Looks like permission expired or was revoked. Please reinstall again.",
"reschedule_reason_success_page": "Reschedule reason",
"choose_common_schedule_team_event": "Choose a common schedule",
"choose_common_schedule_team_event_description": "Enable this if you want to use a common schedule between hosts. When disabled, each host will be booked based on their default schedule.",
"reason": "Reason",
@@ -1412,7 +1426,7 @@
"add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.",
"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",
"booking_limit_reached": "Booking 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.",
@@ -1430,7 +1444,7 @@
"disable_app": "Disable App",
"disable_app_description": "Disabling this app could cause problems with how your users interact with Cal",
"edit_keys": "Edit Keys",
"apps_description": "Enable apps for your instance of Cal",
"admin_apps_description": "Enable apps for your instance of Cal",
"no_available_apps": "There are no available apps",
"no_available_apps_description": "Please ensure there are apps in your deployment under 'packages/app-store'",
"no_apps": "There are no apps enabled in this instance of Cal",
@@ -1440,6 +1454,39 @@
"enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option",
"radio": "Radio",
"kbar_search_placeholder" : "Start typing to search",
"set_as_default": "Set as default"
"individual":"Individual",
"all_bookings_filter_label":"All Bookings",
"all_users_filter_label":"All Users",
"meeting_url_workflow": "Meeting url",
"meeting_url_info": "The event meeting conference url",
"date_overrides": "Date overrides",
"date_overrides_subtitle": "Add dates when your availability changes from your daily hours.",
"date_overrides_info": "Date overrides are archived automatically after the date has passed",
"date_overrides_dialog_which_hours": "Which hours are you free?",
"date_overrides_dialog_which_hours_unavailable": "Which hours are you busy?",
"date_overrides_dialog_title": "Select the dates to override",
"date_overrides_unavailable": "Unavailable all day",
"date_overrides_mark_all_day_unavailable_one": "Mark unavailable (All day)",
"date_overrides_mark_all_day_unavailable_other": "Mark unavailable on selected dates",
"date_overrides_add_btn": "Add Override",
"date_overrides_update_btn": "Update Override",
"event_type_duplicate_copy_text": "{{slug}}-copy",
"set_as_default": "Set as default",
"hide_eventtype_details": "Hide EventType Details",
"show_navigation": "Show navigation",
"hide_navigation": "Hide navigation",
"verification_code_sent": "Verification code sent",
"verified_successfully": "Verified successfully",
"wrong_code": "Wong verification code",
"not_verified": "Not yet verified",
"no_availability_in_month": "No availability in {{month}}",
"view_next_month": "View next month",
"send_code" : "Send code",
"number_verified": "Number Verified",
"create_your_first_team_webhook_description": "Create your first webhook for this team event type",
"create_webhook_team_event_type": "Create a webhook for this team event type",
"disable_success_page":"Disable Success Page (only works if you have a redirect URL)",
"invalid_admin_password": "You are set as an admin but you do not have a password length of at least 15 characters",
"change_password_admin": "Change Password to gain admin access",
"username_already_taken": "Username is already taken"
}

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