Compare commits

..
71 Commits
Author SHA1 Message Date
Omar López 6f9011a167 Update package.json
release.yml / release (push) Canceled after 0s
2023-01-05 18:41:30 -07:00
Omar López ae004c8a6c Fixes extension check for URL with dots in segments (#6301) 2023-01-05 16:38:44 -07:00
Omar López ccbb16fe76 Upgrades prisma (#6298) 2023-01-05 16:24:17 -07:00
Omar López b77ce4a010 Allows to override default debug level for logger (#6300) 2023-01-05 16:23:50 -07:00
Omar Lópezandalannnc f72f709e34 Adds availablity and schedule indexes (#6296)
Co-authored-by: alannnc <alannnc@gmail.com>
2023-01-05 22:29:03 +00:00
Hariom Balhara 37ceb25f8c Make sure returnTo is correct URL (#6189)
* make sure returno is safe

* More fixes
2023-01-05 12:55:55 -07:00
sean-brydon 0141d8f954 Adding border to tabs (#6284)
* Adding border to tabs

* Fix rounded

* Fixing mb on >= lg
2023-01-05 17:14:27 +00:00
sean-brydon 47fe791006 Tidy up v2 layouts (#6266)
* Lots of places use layouts huh

* Fix imports

* FIx merge removing rtl settings

* Fixing Icon Import

* Fix shell import

* Fix tab exports
2023-01-05 18:00:16 +01:00
Jeroen Reumkensandsean-brydon 0cbaba73e4 🧹 Chore/move v2 components to components dir (#6261)
* Removed old root UI components.

* Fixed import path

* Moved first v2 components to new components dir.

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

* Removed irrelevant todo

* Added docs for new components

* Fixed type errors

* Update packages/ui/components/index.ts

Removing as per comment

Co-authored-by: sean-brydon <55134778+sean-brydon@users.noreply.github.com>
2023-01-05 12:04:28 +00:00
Hariom Balhara aaac491dc7 HotFix: Admin Apps Listing (#6282) 2023-01-05 17:20:41 +05:30
zomars fc4203e963 Update api 2023-01-04 16:05:48 -07:00
Hariom Balhara 8430e9612e Revive getSchedule Tests and add more (#6200)
* Revive getSchedule Tests

* Cleanup tests

* More Tests

* Add tests for date override
2023-01-04 15:40:06 -07:00
Jeff Loiselle de81934619 adds devcontainer/codespaces support (#6250) 2023-01-04 15:29:25 -07:00
Jeff Loiselle 5aedcbee55 adds render.com deploy button (#6276) 2023-01-04 23:22:07 +01:00
7bc8d7e991 New Crowdin translations by Github Action (#6274)
Co-authored-by: Crowdin Bot <support+bot@crowdin.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-04 23:20:54 +01:00
LoneRifleandPeer Richelsen df183f5882 chore(web): update .nvmrc to node 16 (#6214)
Follow up on #2711 by ensuring that `app/web/.nvmrc` lines up with `.nvmrc`

Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2023-01-04 15:20:05 -07:00
27d9fad292 Feature/whitelabel part 2 (#5821)
* applied whitelabel to additional parts of the app

* format .md files

* updated metadata of all apps

* applied whitelabel to PoweredByCal.tsx

* fixed `team_upgrade_banner_action` with better german

* fixed import of APP_NAME in email templates

* revert "applied whitelabel" in CalEventParser.test.ts

* Revert "updated metadata of all apps"

This reverts commit 9c1621788d2c4c4a4e4a545f48898d4af7a39f33.

* revert app-store whitelabel changes

* Revert "applied whitelabel to PoweredByCal.tsx"

This reverts commit f2b2d79aa51cca3a70af6bf649b24e22f2fca73a.

* Update website

* Sync

* Update constants.ts

* Prevent unit test error

Co-authored-by: René Müller <rene.mueller@clicksports.de>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: zomars <zomars@me.com>
2023-01-04 22:14:46 +00:00
zomars bed798d440 Formatting 2023-01-04 13:49:20 -07:00
zomars e5a8a4e34d Submodule sync 2023-01-04 13:47:13 -07:00
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
363 changed files with 5517 additions and 10826 deletions
+42
View File
@@ -0,0 +1,42 @@
// For format details, see https://aka.ms/devcontainer.json. For config options, see the
// README at: https://github.com/devcontainers/templates/tree/main/src/docker-outside-of-docker
{
"name": "Docker outside of Docker",
// Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile
"image": "mcr.microsoft.com/devcontainers/base:bullseye",
"features": {
"ghcr.io/devcontainers/features/docker-from-docker:1": {
"version": "latest",
"enableNonRootDocker": "true",
"moby": "true"
},
"ghcr.io/devcontainers/features/node:1": {},
"ghcr.io/devcontainers-contrib/features/npm-package:1": {},
"ghcr.io/devcontainers-contrib/features/jest:2": {},
"ghcr.io/devcontainers-contrib/features/prisma:2": {},
"ghcr.io/guiyomh/features/vim:0": {}
},
// Use this environment variable if you need to bind mount your local source code into a new container.
"remoteEnv": {
"LOCAL_WORKSPACE_FOLDER": "${localWorkspaceFolder}"
},
"hostRequirements": {
"cpus": 4,
"memory": "8gb"
},
// Use 'postCreateCommand' to run commands after the container is created.
"postCreateCommand": "./deploy/install.sh"
// Use 'forwardPorts' to make a list of ports inside the container available locally.
// "forwardPorts": [],
// Configure tool-specific properties.
// "customizations": {},
// Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root.
// "remoteUser": "root"
}
+1
View File
@@ -0,0 +1 @@
🚀 Welcome to Cal.com! Try typing 'yarn dx' to get a quick dev envionment.
+7 -5
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>
@@ -294,6 +293,10 @@ Currently Vercel Pro Plan is required to be able to Deploy this application with
[![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)
### Render
[![Deploy to Render](https://render.com/images/deploy-to-render-button.svg)](https://render.com/deploy?repo=https://github.com/calcom/docker)
<!-- ROADMAP -->
## Roadmap
@@ -310,7 +313,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
@@ -354,7 +357,6 @@ following
5. Use **Application (client) ID** as the **MS_GRAPH_CLIENT_ID** attribute value in .env
6. Click **Certificates & secrets** create a new client secret and use the value as the **MS_GRAPH_CLIENT_SECRET** attribute
### Obtaining Zoom Client ID and Secret
1. Open [Zoom Marketplace](https://marketplace.zoom.us/) and sign in with your Zoom account.
+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";
```
+1 -1
View File
@@ -1 +1 @@
14.17
v16
+3 -3
View File
@@ -639,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={() => {
@@ -914,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}
@@ -1010,7 +1010,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
</CollapsibleTrigger>
<CollapsibleContent>
<div className="mt-6 text-sm">
<div className="mb-4 flex items-center justify-start space-x-2">
<div className="mb-4 flex items-center justify-start space-x-2 rtl:space-x-reverse">
<Switch
checked={previewState.hideEventTypeDetails}
onCheckedChange={(checked) => {
+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;
+1 -3
View File
@@ -11,9 +11,7 @@ import { APP_NAME, COMPANY_NAME, SUPPORT_MAIL_ADDRESS } from "@calcom/lib/consta
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { App as AppType } from "@calcom/types/App";
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText } from "@calcom/ui";
import HeadSeo from "@components/seo/head-seo";
import { Button, Icon, Shell, showToast, SkeletonButton, SkeletonText, HeadSeo } from "@calcom/ui";
const Component = ({
name,
@@ -15,7 +15,7 @@ import {
List,
ShellSubHeading,
showToast,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
Switch,
} from "@calcom/ui";
@@ -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")}
@@ -3,11 +3,7 @@ import { useRouter } from "next/router";
import React, { ComponentProps } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { EmptyScreen, Icon } from "@calcom/ui";
import Shell from "../Shell";
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
import HorizontalTabs from "../navigation/tabs/HorizontalTabs";
import { EmptyScreen, Icon, Shell, HorizontalTabs, HorizontalTabItemProps } from "@calcom/ui";
const tabs: HorizontalTabItemProps[] = [
{
@@ -3,11 +3,9 @@ import React, { ComponentProps } from "react";
import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavigation";
import { InstalledAppVariants } from "@calcom/app-store/utils";
import { trpc } from "@calcom/trpc/react";
import { Icon } from "../../../Icon";
import Shell from "../Shell";
import type { HorizontalTabItemProps } from "../navigation/tabs/HorizontalTabItem";
import type { VerticalTabItemProps } from "../navigation/tabs/VerticalTabItem";
import { Icon, Shell } from "@calcom/ui";
import type { HorizontalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/HorizontalTabItem";
import type { VerticalTabItemProps } from "@calcom/ui/v2/core/navigation/tabs/VerticalTabItem";
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
{
@@ -2,9 +2,9 @@ import { useSession } from "next-auth/react";
import { useRouter } from "next/router";
import React, { ComponentProps, useEffect } from "react";
import ErrorBoundary from "../../../ErrorBoundary";
import Shell from "../Shell";
import SettingsLayout from "./SettingsLayout";
import SettingsLayout from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorBoundary, Shell } from "@calcom/ui";
import { UserPermissionRole } from ".prisma/client";
export default function AdminLayout({
@@ -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>
);
@@ -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 = {
@@ -95,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"
)}
/>
@@ -112,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>
@@ -127,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]"
)}
/>
@@ -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",
@@ -206,7 +220,7 @@ function BookingListItem(booking: BookingItemProps) {
},
});
};
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
return (
<>
<RescheduleDialog
@@ -220,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>
@@ -342,7 +363,7 @@ function BookingListItem(booking: BookingItemProps) {
<span> </span>
{!!booking?.eventType?.price && !booking.paid && (
<Badge className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex" variant="orange">
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
{t("pending_payment")}
</Badge>
)}
@@ -377,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 />
@@ -17,6 +17,7 @@ import {
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";
@@ -27,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, HeadSeo } from "@calcom/ui";
import { timeZone as localStorageTimeZone } from "@lib/clock";
import useRouterQuery from "@lib/hooks/useRouterQuery";
@@ -36,7 +37,6 @@ import Gates, { Gate, GateState } from "@components/Gates";
import AvailableTimes from "@components/booking/AvailableTimes";
import BookingDescription from "@components/booking/BookingDescription";
import TimeOptions from "@components/booking/TimeOptions";
import { HeadSeo } from "@components/seo/head-seo";
import PoweredByCal from "@components/ui/PoweredByCal";
import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
@@ -220,7 +220,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" />
@@ -367,7 +367,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
<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 " />
<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 })}
@@ -392,7 +392,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
)}
{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" />
<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}
@@ -413,7 +413,7 @@ 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>
@@ -501,7 +501,7 @@ const BookingPage = ({
<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" />
<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}
@@ -513,7 +513,7 @@ const BookingPage = ({
)}
{!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" />
<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,
@@ -524,7 +524,7 @@ const BookingPage = ({
</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" />
<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 &&
@@ -552,7 +552,7 @@ const BookingPage = ({
{t("former_time")}
</p>
<p className="line-through ">
<Icon.FiCalendar className="mr-[10px] ml-[2px] -mt-1 inline-block h-4 w-4" />
<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>
@@ -560,7 +560,7 @@ const BookingPage = ({
{!!eventType.seatsPerTimeSlot && (
<div className="text-bookinghighlight flex items-start text-sm">
<Icon.FiUser
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
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
@@ -623,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>
)}
@@ -664,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>
);
@@ -689,7 +689,7 @@ const BookingPage = ({
{selectedLocationType === LocationType.Phone
? t("phone_number")
: selectedLocationType === LocationType.AttendeeInPerson
? t("Address")
? t("address")
: ""}
</label>
<div className="mt-1">
@@ -716,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>
)}
@@ -800,6 +800,7 @@ const BookingPage = ({
{input.options && input.type === EventTypeCustomInputType.RADIO && (
<div className="flex">
<Group
name={`customInputs.${input.id}`}
required={input.required}
onValueChange={(e) => {
bookingForm.setValue(`customInputs.${input.id}`, e);
@@ -808,9 +809,9 @@ const BookingPage = ({
{input.options.map((option, i) => (
<RadioField
label={option.label}
key={`option.${i}.radio`}
key={`option.${input.id}.${i}.radio`}
value={option.label}
id={`option.${i}.radio`}
id={`option.${input.id}.${i}.radio`}
/>
))}
</>
@@ -833,7 +834,7 @@ const BookingPage = ({
/>
{bookingForm.formState.errors?.customInputs?.[input.id] && (
<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>
)}
@@ -913,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
@@ -134,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">
@@ -290,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>
@@ -298,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">
@@ -345,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>
);
};
@@ -1,5 +1,6 @@
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
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";
@@ -13,7 +14,7 @@ import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@ca
export const EventTeamWebhooksTab = ({
eventType,
team,
}: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
}: Pick<EventTypeSetupProps, "eventType" | "team">) => {
const { t } = useLocale();
const utils = trpc.useContext();
@@ -117,7 +118,7 @@ export const EventTeamWebhooksTab = ({
</>
) : (
<EmptyScreen
Icon={Icon.FiLink}
Icon={TbWebhook}
headline={t("create_your_first_webhook")}
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
buttonRaw={<NewWebhookButton />}
@@ -1,8 +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 { UseFormReturn } from "react-hook-form";
import { TbWebhook } from "react-icons/tb";
import { classNames } from "@calcom/lib";
import { CAL_URL } from "@calcom/lib/constants";
@@ -36,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;
@@ -159,7 +160,7 @@ function EventTypeSingleLayout({
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
icon: Icon.FiLink,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
});
}
@@ -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,
})}
@@ -3,7 +3,7 @@ import { useRouter } from "next/router";
import React, { useEffect, useState } from "react";
import { Toaster } from "react-hot-toast";
import { StepCard, Steps } from "../../..";
import { StepCard, Steps } from "@calcom/ui";
export default function WizardLayout({
children,
+4
View File
@@ -0,0 +1,4 @@
We don't have many layouts currently however, if they are relevant to a features - please put them in their relvant component folder
in `web/components` if you believe they will be reused in other apps please place them in `packages/features/[feature]`
Thanks Sean 🔥
@@ -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>
+6 -3
View File
@@ -1,10 +1,9 @@
import classNames from "classnames";
import React from "react";
import { APP_NAME, LOGO } from "@calcom/lib/constants";
import { Credits, HeadSeo } from "@calcom/ui";
import Loader from "@components/Loader";
import { HeadSeo } from "@components/seo/head-seo";
interface Props {
title: string;
@@ -23,6 +22,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 +35,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);
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "2.4.1",
"version": "2.4.4",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
+1 -3
View File
@@ -4,12 +4,10 @@ import { useRouter } from "next/router";
import { useEffect, useState } from "react";
import { COMPANY_NAME, DEVELOPER_DOCS, DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
import { Icon } from "@calcom/ui";
import { Icon, HeadSeo } from "@calcom/ui";
import { useLocale } from "@lib/hooks/useLocale";
import { HeadSeo } from "@components/seo/head-seo";
import { ssgInit } from "@server/lib/ssg";
export default function Custom404() {
+3 -3
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, HeadSeo } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { EmbedProps } from "@lib/withEmbedSsr";
@@ -36,7 +37,6 @@ import { AvatarSSR } from "@components/ui/AvatarSSR";
import { ssrInit } from "@server/lib/ssr";
const HeadSeo = dynamic(() => import("@components/seo/head-seo"));
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
const [user] = users; //To be used when we only have a single user, not dynamic group
@@ -138,7 +138,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>
-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>
+1 -1
View File
@@ -138,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) }),
});
+19 -11
View File
@@ -5,6 +5,7 @@ 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";
@@ -27,7 +28,7 @@ const decryptedSchema = z.object({
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
const { action, token, reason } = querySchema.parse(req.query);
const { bookingUid, userId } = decryptedSchema.parse(
JSON.parse(symmetricDecrypt(token, process.env.CALENDSO_ENCRYPTION_KEY || ""))
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
);
/** We shape the session as required by tRPC router */
@@ -47,17 +48,24 @@ async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
where: { uid: bookingUid },
});
/** @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,
});
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;
}
return res.redirect(`/booking/${bookingUid}`);
res.redirect(`/booking/${bookingUid}`);
}
export default defaultResponder(handler);
+3 -1
View File
@@ -7,7 +7,9 @@ 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 { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
import AppsLayout from "@components/apps/layouts/AppsLayout";
import { ssgInit } from "@server/lib/ssg";
+3 -3
View File
@@ -14,16 +14,16 @@ import {
Button,
EmptyScreen,
Icon,
InstalledAppsLayout,
List,
ShellSubHeading,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { QueryCell } from "@lib/QueryCell";
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
import IntegrationListItem from "@components/apps/IntegrationListItem";
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
function ConnectOrDisconnectIntegrationButton(props: {
credentialIds: number[];
@@ -32,7 +32,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
installed?: boolean;
invalidCredentialIds?: number[];
}) {
const { type, credentialIds, isGlobal, installed, invalidCredentialIds } = props;
const { type, credentialIds, isGlobal, installed } = props;
const { t } = useLocale();
const [credentialId] = credentialIds;
+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")}
@@ -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>
)
+2 -2
View File
@@ -6,7 +6,7 @@ import * as React from "react";
import { useEffect, useState, useRef } from "react";
import z from "zod";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { APP_NAME, WEBAPP_URL } from "@calcom/lib/constants";
import { trpc } from "@calcom/trpc/react";
import { Button, showToast } from "@calcom/ui";
@@ -110,7 +110,7 @@ export default function Verify() {
? "Your payment failed"
: sessionId
? "Payment successful!"
: "Verify your email" + " | Cal.com"}
: "Verify your email" + " | " + APP_NAME}
</title>
</Head>
<div className="flex min-h-screen flex-col items-center justify-center px-6">
+11 -4
View File
@@ -35,14 +35,13 @@ import { localStorage } from "@calcom/lib/webstorage";
import prisma, { baseUserSelect } from "@calcom/prisma";
import { Prisma } from "@calcom/prisma/client";
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
import { Button, EmailInput, Icon } from "@calcom/ui";
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui";
import { timeZone } from "@lib/clock";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import CancelBooking from "@components/booking/CancelBooking";
import EventReservationSchema from "@components/schemas/EventReservationSchema";
import { HeadSeo } from "@components/seo/head-seo";
import { ssrInit } from "@server/lib/ssr";
@@ -174,7 +173,6 @@ const querySchema = z.object({
export default function Success(props: SuccessProps) {
const { t } = useLocale();
const router = useRouter();
const {
allRemainingBookings,
isSuccessBookingPage,
@@ -636,7 +634,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"
@@ -1051,6 +1049,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() })}
+57 -262
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";
@@ -40,7 +38,6 @@ 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 = {
@@ -109,27 +106,24 @@ const querySchema = z.object({
.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,
@@ -137,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) {
@@ -204,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;
@@ -230,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} />,
@@ -261,7 +261,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
isUpdateMutationLoading={updateMutation.isLoading}
formMethods={formMethods}
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
currentUserMembership={props.currentUserMembership}>
currentUserMembership={currentUserMembership}>
<Form
form={formMethods}
id="event-type-form"
@@ -326,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 });
@@ -347,244 +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,
webhooks: {
select: {
id: true,
subscriberUrl: true,
payloadTemplate: true,
active: true,
eventTriggers: true,
secret: true,
eventTypeId: 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;
+8 -8
View File
@@ -4,10 +4,12 @@ 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 isCalcom from "@calcom/lib/isCalcom";
import { RouterOutputs, trpc, TRPCClientError } from "@calcom/trpc/react";
import {
Badge,
@@ -23,13 +25,11 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
EmptyScreen,
EventTypeDescription,
Icon,
Shell,
showToast,
Switch,
Tooltip,
TipBanner,
} from "@calcom/ui";
import { withQuery } from "@lib/QueryCell";
@@ -78,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>
)}
@@ -394,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} />
@@ -531,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">
@@ -589,7 +589,6 @@ const EventTypesPage = () => {
customLoader={<SkeletonLoader />}
success={({ data }) => (
<>
{isCalcom && <TipBanner />}
{data.eventTypeGroups.map((group, index) => (
<Fragment key={group.profile.slug}>
{/* hide list heading when there is only one (current user) */}
@@ -608,6 +607,7 @@ const EventTypesPage = () => {
/>
</Fragment>
))}
{data.eventTypeGroups.length === 0 && <CreateFirstEventTypeView />}
<EmbedDialog />
</>
@@ -2,7 +2,9 @@ import { GetServerSidePropsContext } from "next";
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
@@ -11,7 +13,7 @@ function AdminAppsView() {
return (
<>
<Meta title={t("apps")} description={t("admin_apps_description")} />
<AdminAppsList baseURL="/settings/admin/apps" className="w-0" />
<AdminAppsList baseURL="/settings/admin/apps" />
</>
);
}
@@ -0,0 +1,2 @@
export { getServerSideProps } from "./[category]";
export { default } from "./[category]";
@@ -3,7 +3,9 @@ import { signIn } from "next-auth/react";
import { useRef } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, getAdminLayout as getLayout, Meta, TextField } from "@calcom/ui";
import { Button, Meta, TextField } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
@@ -21,7 +23,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")}
+3 -1
View File
@@ -1,6 +1,8 @@
import { GetServerSidePropsContext } from "next";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
+3 -1
View File
@@ -1,7 +1,9 @@
import { GetServerSidePropsContext } from "next";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getAdminLayout as getLayout, Meta } from "@calcom/ui";
import { Meta } from "@calcom/ui";
import { getLayout } from "@components/auth/layouts/AdminLayout";
import { ssrInit } from "@server/lib/ssr";
+2 -1
View File
@@ -3,11 +3,12 @@ import { useRouter } from "next/router";
import { useState } from "react";
import { HelpScout, useChat } from "react-live-chat-loader";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { classNames } from "@calcom/lib";
import { WEBAPP_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, getSettingsLayout as getLayout, Icon, Meta } from "@calcom/ui";
import { Button, Icon, Meta } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -5,6 +5,7 @@ import { TApiKeys } from "@calcom/ee/api-keys/components/ApiKeyListItem";
import LicenseRequired from "@calcom/ee/common/components/v2/LicenseRequired";
import ApiKeyDialogForm from "@calcom/features/ee/api-keys/components/ApiKeyDialogForm";
import ApiKeyListItem from "@calcom/features/ee/api-keys/components/ApiKeyListItem";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -13,10 +14,9 @@ import {
Dialog,
DialogContent,
EmptyScreen,
getSettingsLayout as getLayout,
Icon,
Meta,
SkeletonLoader,
AppSkeletonLoader as SkeletonLoader,
} from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -2,6 +2,7 @@ import { GetServerSidePropsContext } from "next";
import { useSession } from "next-auth/react";
import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
@@ -10,7 +11,6 @@ import {
Button,
ColorPicker,
Form,
getSettingsLayout as getLayout,
Meta,
showToast,
SkeletonButton,
@@ -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>
@@ -6,6 +6,7 @@ import { Fragment } from "react";
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -13,7 +14,6 @@ import {
Badge,
Button,
EmptyScreen,
getSettingsLayout as getLayout,
Icon,
List,
ListItem,
@@ -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>
@@ -1,6 +1,7 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
@@ -13,7 +14,6 @@ import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
getSettingsLayout as getLayout,
Icon,
List,
ListItem,
@@ -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>
@@ -3,12 +3,12 @@ import { useRouter } from "next/router";
import { useMemo } from "react";
import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { RouterOutputs, trpc } from "@calcom/trpc/react";
import {
Button,
Form,
getSettingsLayout as getLayout,
Label,
Meta,
Select,
@@ -5,6 +5,7 @@ import { signOut } from "next-auth/react";
import { BaseSyntheticEvent, useRef, useState } from "react";
import { Controller, useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { ErrorCode } from "@calcom/lib/auth";
import { APP_NAME } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
@@ -21,7 +22,6 @@ import {
DialogFooter,
DialogTrigger,
Form,
getSettingsLayout as getLayout,
Icon,
ImageUploader,
Label,
@@ -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"
@@ -1,18 +1,10 @@
import { GetServerSidePropsContext } from "next";
import { useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Button,
Form,
getSettingsLayout as getLayout,
Label,
Meta,
showToast,
Skeleton,
Switch,
} from "@calcom/ui";
import { Button, Form, Label, Meta, showToast, Skeleton, Switch } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -3,10 +3,11 @@ import { GetServerSidePropsContext } from "next";
import { Trans } from "next-i18next";
import { useForm } from "react-hook-form";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { identityProviderNameMap } from "@calcom/lib/auth";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import { Button, Form, getSettingsLayout as getLayout, Meta, PasswordField, showToast } from "@calcom/ui";
import { Button, Form, Meta, PasswordField, showToast } from "@calcom/ui";
import { ssrInit } from "@server/lib/ssr";
@@ -1,17 +1,10 @@
import { GetServerSidePropsContext } from "next";
import { useState } from "react";
import { getLayout } from "@calcom/features/settings/layouts/SettingsLayout";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { trpc } from "@calcom/trpc/react";
import {
Badge,
getSettingsLayout as getLayout,
Meta,
Switch,
SkeletonButton,
SkeletonContainer,
SkeletonText,
} from "@calcom/ui";
import { Badge, Meta, Switch, SkeletonButton, SkeletonContainer, SkeletonText } from "@calcom/ui";
import DisableTwoFactorModal from "@components/settings/DisableTwoFactorModal";
import EnableTwoFactorModal from "@components/settings/EnableTwoFactorModal";
@@ -2,7 +2,8 @@ import Head from "next/head";
import AddNewTeamMembers from "@calcom/features/ee/teams/components/AddNewTeamMembers";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { WizardLayout } from "@calcom/ui";
import WizardLayout from "@components/layouts/WizardLayout";
const OnboardTeamMembersPage = () => {
const { t } = useLocale();
+2 -1
View File
@@ -3,7 +3,8 @@ import Head from "next/head";
import { CreateANewTeamForm } from "@calcom/features/ee/teams/components";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { getWizardLayout as getLayout } from "@calcom/ui";
import { getLayout } from "@components/layouts/WizardLayout";
import { ssrInit } from "@server/lib/ssr";
+1 -2
View File
@@ -8,8 +8,7 @@ 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";
import { Alert, Button, EmailField, PasswordField, TextField, HeadSeo } from "@calcom/ui";
import { asStringOrNull } from "@calcom/web/lib/asStringOrNull";
import { WEBAPP_URL } from "@calcom/web/lib/config/constants";
import prisma from "@calcom/web/lib/prisma";
+3 -3
View File
@@ -5,18 +5,18 @@ 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, HeadSeo } from "@calcom/ui";
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { HeadSeo } from "@components/seo/head-seo";
import Team from "@components/team/screens/Team";
import AvatarGroup from "@components/ui/AvatarGroup";
@@ -48,7 +48,7 @@ function TeamPage({ team }: TeamPageProps) {
<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} />
+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?returnTo=/teams`}>
<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=${WEBAPP_URL}/teams`}>
{t("new")}
</Button>
}>
+5 -5
View File
@@ -4,7 +4,7 @@ import { getSession } from "next-auth/react";
import Head from "next/head";
import { useEffect } from "react";
import { SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
import { APP_NAME, SEO_IMG_OGIMG_VIDEO, WEBSITE_URL } from "@calcom/lib/constants";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { inferSSRProps } from "@calcom/types/inferSSRProps";
@@ -48,18 +48,18 @@ export default function JoinCall(props: JoinCallPageProps) {
return (
<>
<Head>
<title>Cal.com Video</title>
<meta name="title" content="Cal.com Video" />
<title>{APP_NAME} Video</title>
<meta name="title" content={APP_NAME + " Video"} />
<meta name="description" content={t("quick_video_meeting")} />
<meta property="og:image" content={SEO_IMG_OGIMG_VIDEO} />
<meta property="og:type" content="website" />
<meta property="og:url" content={`${WEBSITE_URL}/video`} />
<meta property="og:title" content="Cal.com Video" />
<meta property="og:title" content={APP_NAME + " Video"} />
<meta property="og:description" content={t("quick_video_meeting")} />
<meta property="twitter:image" content={SEO_IMG_OGIMG_VIDEO} />
<meta property="twitter:card" content="summary_large_image" />
<meta property="twitter:url" content={`${WEBSITE_URL}/video`} />
<meta property="twitter:title" content="Cal.com Video" />
<meta property="twitter:title" content={APP_NAME + " Video"} />
<meta property="twitter:description" content={t("quick_video_meeting")} />
</Head>
<div style={{ zIndex: 2, position: "relative" }}>
+1 -3
View File
@@ -4,12 +4,10 @@ import dayjs from "@calcom/dayjs";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import { Button, Icon } from "@calcom/ui";
import { Button, Icon, HeadSeo } from "@calcom/ui";
import { inferSSRProps } from "@lib/types/inferSSRProps";
import { HeadSeo } from "@components/seo/head-seo";
export default function MeetingUnavailable(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
@@ -5,9 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
import { Button, Icon } from "@calcom/ui";
import { HeadSeo } from "@components/seo/head-seo";
import { Button, Icon, HeadSeo } from "@calcom/ui";
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
const { t } = useLocale();
+1 -3
View File
@@ -1,7 +1,5 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button, Icon } from "@calcom/ui";
import { HeadSeo } from "@components/seo/head-seo";
import { Button, Icon, HeadSeo } from "@calcom/ui";
export default function NoMeetingFound() {
const { t } = useLocale();
@@ -457,6 +457,7 @@
"email_address": "عنوان البريد الإلكتروني",
"enter_valid_email": "يرجى إدخال بريد إلكتروني صالح",
"location": "الموقع",
"in_person_attendee_address": "شخصيًا (عنوان من سيحضر)",
"yes": "نعم",
"no": "لا",
"additional_notes": "ملاحظات إضافية",
@@ -1390,7 +1391,7 @@
"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": "معرّف المرسل",
@@ -457,6 +457,7 @@
"email_address": "E-mailová adresa",
"enter_valid_email": "Zadejte platný e-mail",
"location": "Místo",
"in_person_attendee_address": "Osobně (adresa účastníka)",
"yes": "ano",
"no": "ne",
"additional_notes": "Doplňující poznámky",
@@ -456,6 +456,9 @@
"email_address": "E-Mail Adresse",
"enter_valid_email": "Bitte geben Sie eine gültige Emaildresse 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",
+20 -2
View File
@@ -388,7 +388,7 @@
"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",
"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.",
"edit_logo": "Edit logo",
@@ -460,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",
@@ -1091,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",
@@ -1148,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",
@@ -1221,6 +1231,7 @@
"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",
@@ -1443,6 +1454,9 @@
"enter_option": "Enter Option {{index}}",
"add_an_option": "Add an option",
"radio": "Radio",
"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",
@@ -1470,5 +1484,9 @@
"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"
"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"
}
@@ -457,6 +457,7 @@
"email_address": "Email",
"enter_valid_email": "Ingresa un correo electrónico válido",
"location": "Ubicación",
"in_person_attendee_address": "En persona (dirección del asistente)",
"yes": "Sí",
"no": "No",
"additional_notes": "Notas Adicionales",
@@ -457,6 +457,7 @@
"email_address": "Adresse e-mail",
"enter_valid_email": "Veuillez saisir une adresse e-mail valide",
"location": "Localisation",
"in_person_attendee_address": "En personne (adresse du participant)",
"yes": "oui",
"no": "non",
"additional_notes": "Notes additionnelles",
@@ -457,6 +457,7 @@
"email_address": "כתובת דוא\"ל",
"enter_valid_email": "יש להזין כתובת דוא״ל תקינה",
"location": "מיקום",
"in_person_attendee_address": "פנים-אל-פנים (הכתובת של המשתתף/ת)",
"yes": "כן",
"no": "לא",
"additional_notes": "הערות נוספות",
@@ -611,7 +612,7 @@
"monthly_other": "חודשים",
"yearly_one": "שנה",
"yearly_other": "שנים",
"plus_more": "ועוד {{count}}",
"plus_more": "{{count}} נוספים",
"max": "מקסימום",
"single_theme": "ערכת נושא בודדת",
"brand_color": "צבע המותג",
@@ -457,6 +457,7 @@
"email_address": "Indirizzo email",
"enter_valid_email": "Immettere un indirizzo e-mail valido",
"location": "Luogo",
"in_person_attendee_address": "Di persona (indirizzo del partecipante)",
"yes": "sì",
"no": "no",
"additional_notes": "Note aggiuntive",
@@ -457,6 +457,7 @@
"email_address": "メールアドレス",
"enter_valid_email": "有効なメールアドレスを入力してください",
"location": "所在地",
"in_person_attendee_address": "対面(出席者のアドレス)",
"yes": "はい",
"no": "いいえ",
"additional_notes": "追加メモ",
@@ -457,6 +457,7 @@
"email_address": "이메일 주소",
"enter_valid_email": "유효한 이메일을 입력하세요",
"location": "위치",
"in_person_attendee_address": "직접 방문(참석자 주소)",
"yes": "네",
"no": "아니요",
"additional_notes": "추가 참고 사항",
@@ -457,6 +457,7 @@
"email_address": "E-mail adres",
"enter_valid_email": "Voer een geldig e-mailadres in",
"location": "Locatie",
"in_person_attendee_address": "Persoonlijk (adres deelnemer)",
"yes": "ja",
"no": "nee",
"additional_notes": "Aanvullende notities",
+43 -1
View File
@@ -303,6 +303,7 @@
"cancelling_all_recurring": "Dette er alle gjenværende forekomster i den gjentakende hendelsen.",
"error_with_status_code_occured": "Det oppstod en feil med statuskoden {{status}}.",
"booking_already_cancelled": "Denne bookingen var allerede kansellert",
"booking_already_accepted_rejected": "Denne bookingen var allerede akseptert eller avvist",
"go_back_home": "Gå til hjem",
"or_go_back_home": "Eller gå tilbake til hjem",
"no_availability": "Utilgjengelig",
@@ -385,6 +386,8 @@
"go_to_billing_portal": "Gå til betalingsportalen",
"need_anything_else": "Trenger du noe annet?",
"further_billing_help": "Hvis du trenger ytterligere hjelp med betaling, er supportteamet vårt her for å hjelpe.",
"contact": "Kontakt",
"our_support_team": "vårt support-team",
"contact_our_support_team": "Kontakt supportteamet vårt",
"uh_oh": "Uh oh!",
"no_event_types_have_been_setup": "Denne brukeren har ikke konfigurert noen hendelsestyper ennå.",
@@ -457,6 +460,7 @@
"email_address": "Epost adresse",
"enter_valid_email": "Vennligst skriv inn en gyldig e-postadresse",
"location": "Sted",
"in_person_attendee_address": "Personlig Oppmøte (Deltakers Adresse)",
"yes": "ja",
"no": "nei",
"additional_notes": "Tilleggsinformasjon",
@@ -629,6 +633,7 @@
"teams": "Team",
"team": "Team",
"team_billing": "Team Fakturering",
"team_billing_description": "Administrer betaling for ditt team",
"upgrade_to_flexible_pro_title": "Vi har endret fakturering for team",
"upgrade_to_flexible_pro_message": "Det er medlemmer i teamet ditt uten sete. Oppgrader proffplanen din for å dekke manglende seter.",
"changed_team_billing_info": "Fra januar 2022 tar vi betalt per sete for teammedlemmer. Medlemmer av teamet ditt som hadde PRO gratis, har nå en 14 dagers prøveversjon. Når prøveperioden deres utløper, vil disse medlemmene bli skjult for teamet ditt med mindre du oppgraderer nå.",
@@ -704,6 +709,7 @@
"hide_event_type": "Skjul hendelsestype",
"edit_location": "Rediger sted",
"into_the_future": "i fremtiden",
"when_booked_with_less_than_notice": "Når booket med mindre enn <time></time> varsel",
"within_date_range": "Innenfor en datoperiode",
"indefinitely_into_future": "Uendelig inn i fremtiden",
"add_new_custom_input_field": "Legg til nytt egendefinert input felt",
@@ -723,6 +729,8 @@
"delete_account_confirmation_message": "Er du sikker på at du vil slette {{appName}}-kontoen din? Alle du har delt konto-lenken med, vil ikke lenger kunne booke ved å bruke den, og eventuelle innstillinger du har lagret vil gå tapt.",
"integrations": "Integrasjoner",
"apps": "Apper",
"apps_description": "Her finner du en liste over appene dine",
"apps_listing": "App liste",
"category_apps": "{{category}} apper",
"app_store": "App Butikken",
"app_store_description": "Kobler sammen folk, teknologi og arbeidsplassen.",
@@ -746,6 +754,7 @@
"toggle_calendars_conflict": "Huk av kalenderne du vil sjekke for konfliker for å hindre dobbelt-bookinger.",
"select_destination_calendar": "Opprett hendelser på",
"connect_additional_calendar": "Koble til enda en kalender",
"calendar_updated_successfully": "Kalenderen er oppdatert",
"conferencing": "Konferansering",
"calendar": "Kalender",
"payments": "Betalinger",
@@ -1028,6 +1037,9 @@
"error_removing_app": "Feil ved fjerning av app",
"web_conference": "Nett-konferanse",
"requires_confirmation": "Krever bekreftelse",
"always_requires_confirmation": "Alltid",
"requires_confirmation_threshold": "Krever bekreftelse hvis booket med < {{time}} $t({{unit}}_timeUnit) varsel",
"may_require_confirmation": "Kan kreve bekreftelse",
"nr_event_type_one": "{{count}} hendelsestype",
"nr_event_type_other": "{{count}} hendelsestyper",
"add_action": "Legg til handling",
@@ -1080,6 +1092,10 @@
"navigate": "Naviger",
"open": "Åpne",
"close": "Lukk",
"upgrade": "Oppgrader",
"upgrade_to_access_recordings_title": "Oppgrader for å få tilgang til opptak",
"upgrade_to_access_recordings_description": "Opptak er kun tilgjengelig som en del av team-planen vår. Oppgrader for å begynne å ta opp samtalene dine",
"recordings_are_part_of_the_teams_plan": "Opptak er en del av team-planen",
"team_feature_teams": "Dette er en Team-funksjon. Oppgrader til Team for å se teamets tilgjengelighet.",
"team_feature_workflows": "Dette er en Team-funksjon. Oppgrader til Team for å automatisere hendelsesvarslene og påminnelsene dine med arbeidsflyter.",
"show_eventtype_on_profile": "Vis på Profil",
@@ -1137,8 +1153,11 @@
"confirm_delete_account_modal": "Er du sikker på at du vil slette {{appName}}-kontoen din?",
"delete_my_account": "Slett kontoen min",
"start_of_week": "Begynnelsen av uken",
"recordings_title": "Opptak",
"recording": "Opptak",
"select_calendars": "Velg hvilke kalendere du vil sjekke for konflikter for å forhindre dobbeltbookinger.",
"check_for_conflicts": "Se etter konflikter",
"view_recordings": "Vis opptak",
"adding_events_to": "Legger til hendelser til",
"follow_system_preferences": "Følg systempreferansene",
"custom_brand_colors": "Egendefinerte merkevare-farger",
@@ -1171,8 +1190,11 @@
"invoices": "Fakturaer",
"embeds": "Innbygginger",
"impersonation": "Etterligning",
"impersonation_description": "Innstillinger for å administrere etterligning av brukere",
"users": "Brukere",
"profile_description": "Administrer innstillinger for {{appName}} profilen din",
"users_description": "Her finner du en liste over alle brukere",
"users_listing": "Bruker liste",
"general_description": "Administrer innstillinger for språk og tidssone",
"calendars_description": "Konfigurer hvordan hendelsestypene dine samhandler med kalenderne dine",
"appearance_description": "Administrer innstillinger for booking-utseendet",
@@ -1207,6 +1229,7 @@
"to": "Til",
"workflow_turned_on_successfully": "Arbeidsflyten for {{workflowName}} ble {{offOn}} slått av",
"download_responses": "Last ned Respons",
"download": "Nedlasting",
"create_your_first_form": "Lag ditt første skjema",
"create_your_first_form_description": "Med Viderekoblings-skjema kan du stille kvalifiserende spørsmål og videresende til riktig person eller hendelsestype.",
"create_your_first_webhook": "Lag din første Webhook",
@@ -1315,6 +1338,8 @@
"booking_confirmation_success": "Booking-bekreftelsen var vellykket",
"booking_rejection_success": "Booking-avvisningen var vellykket",
"booking_confirmation_fail": "Booking-bekreftelse feilet",
"booking_tentative": "Denne bookingen er foreløpig",
"booking_accept_intent": "Oops, jeg vil akseptere",
"we_wont_show_again": "Vi vil ikke vise dette igjen",
"couldnt_update_timezone": "Vi kunne ikke oppdatere tidssonen",
"updated_timezone_to": "Oppdaterte tidssone til {{formattedCurrentTz}}",
@@ -1377,7 +1402,9 @@
"number_sms_notifications": "Telefonnummer (SMS varsler)",
"attendee_email_workflow": "Deltakerens e-post",
"attendee_email_info": "Den som booker sin e-post",
"kbar_search_placeholder": "Skriv inn en kommando eller søk...",
"invalid_credential": "Å nei! Det ser ut til at tillatelsen er utløpt eller tilbakekalt. Vennligst installer på nytt.",
"reschedule_reason_success_page": "Årsak for endring av tidspunkt",
"choose_common_schedule_team_event": "Velg en felles timeplan",
"choose_common_schedule_team_event_description": "Aktiver dette hvis du vil bruke en felles tidsplan mellom verter. Når den er deaktivert, vil hver vert bookes basert på deres standard tidsplan.",
"reason": "Årsak",
@@ -1388,10 +1415,25 @@
"route_to": "Videresend til",
"test_preview_description": "Test viderekoblings-skjemaet ditt uten å sende noen data",
"test_routing": "Test Viderekobling",
"payment_app_disabled": "En administrator har deaktivert en betalingsapp",
"edit_event_type": "Rediger hendelsestype",
"collective_scheduling": "Kollektiv planlegging av tidspunkt",
"make_it_easy_to_book": "Gjør det enkelt å booke teamet ditt når alle er tilgjengelige.",
"find_the_best_person": "Finn den beste personen tilgjengelig og bla gjennom teamet ditt.",
"fixed_round_robin": "Fast round robin",
"add_one_fixed_attendee": "Legg til én fast deltaker og round robin gjennom et antall deltakere.",
"calcom_is_better_with_team": "Cal.com er bedre med team",
"add_your_team_members": "Legg til team-medlemmene dine i hendelsestypene dine. Bruk kollektiv planlegging av tidspunkt for å inkludere alle eller finn den mest passende personen med round robin-planlegging.",
"booking_limit_reached": "Booking-grensen for denne hendelsestypen er nådd",
"admin_has_disabled": "En administrator har deaktivert {{appName}}",
"fill_this_field": "Vennligst fyll ut dette feltet",
"options": "Alternativer",
"enter_option": "Angi alternativ {{index}}",
"add_an_option": "Legg til et alternativ",
"radio": "Radio"
"radio": "Radio",
"create_your_first_team_webhook_description": "Opprett din første webhook for denne team-hendelsestypen",
"create_webhook_team_event_type": "Opprett en webhook for denne team-hendelsestypen",
"disable_success_page": "Deaktiver Suksess-side (fungerer bare hvis du har en omdirigerings-URL)",
"invalid_admin_password": "Du er satt som administrator, men du har ikke en passordlengde på minst 15 tegn",
"change_password_admin": "Endre passord for å få administratortilgang"
}

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