Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8e8c96c267 | ||
|
|
fab765c5ee | ||
|
|
89ed07a097 | ||
|
|
d7d73d2caf | ||
|
|
aa92730e28 | ||
|
|
430430e0cd | ||
|
|
62713940b0 | ||
|
|
75aef09338 | ||
|
|
4e65c30e18 | ||
|
|
8b873e2828 | ||
|
|
e70d179628 | ||
|
|
ca9a836300 | ||
|
|
abdf72dc0f | ||
|
|
227cd165f6 | ||
|
|
a0135cdc59 | ||
|
|
112d504148 | ||
|
|
d275930732 | ||
|
|
97e33bdd7e | ||
|
|
3f13791faf | ||
|
|
359cfc4be0 | ||
|
|
1ad6bba0ab | ||
|
|
6dc4211d81 | ||
|
|
e5f0452728 | ||
|
|
d95bba63f4 | ||
|
|
6f48869021 | ||
|
|
845bfae887 | ||
|
|
3fe02fba26 | ||
|
|
4ca5ca9fe2 | ||
|
|
364e187080 | ||
|
|
b1193eef1e | ||
|
|
e110f12cf1 | ||
|
|
022b513935 | ||
|
|
010f39a7a4 | ||
|
|
6b6eed66eb | ||
|
|
c214e01f03 | ||
|
|
a28ac3634d | ||
|
|
a0709fac88 | ||
|
|
e5b6a3332e | ||
|
|
979686920a | ||
|
|
793850f3aa | ||
|
|
fa9d72fc0a | ||
|
|
0d485ffed6 | ||
|
|
9a2e55d528 | ||
|
|
81ecbb86a5 | ||
|
|
2cd816932b | ||
|
|
d83411162d | ||
|
|
c9c54db3a1 | ||
|
|
d100600789 | ||
|
|
6f9011a167 | ||
|
|
ae004c8a6c | ||
|
|
ccbb16fe76 | ||
|
|
b77ce4a010 | ||
|
|
f72f709e34 | ||
|
|
37ceb25f8c | ||
|
|
0141d8f954 | ||
|
|
47fe791006 | ||
|
|
0cbaba73e4 | ||
|
|
aaac491dc7 | ||
|
|
fc4203e963 | ||
|
|
8430e9612e | ||
|
|
de81934619 | ||
|
|
5aedcbee55 | ||
|
|
7bc8d7e991 | ||
|
|
df183f5882 | ||
|
|
27d9fad292 | ||
|
|
bed798d440 | ||
|
|
e5a8a4e34d | ||
|
|
de446db434 | ||
|
|
81546412bf | ||
|
|
459530fba3 | ||
|
|
42766b42c0 | ||
|
|
38b3563350 | ||
|
|
37159914cf | ||
|
|
268bf68532 | ||
|
|
b02c7e01d5 | ||
|
|
45d10a3bc4 | ||
|
|
9250b20c1d | ||
|
|
d9da311141 | ||
|
|
89a52dad21 | ||
|
|
2a1acc1294 | ||
|
|
8302990f84 | ||
|
|
5a03b61d63 | ||
|
|
6b6f239936 | ||
|
|
9c2748e8e9 | ||
|
|
bb5cc33737 | ||
|
|
5616a64827 | ||
|
|
33530ad8c8 | ||
|
|
fe14839f1b | ||
|
|
15123c1aa5 | ||
|
|
71ee9c72fe | ||
|
|
5503539856 | ||
|
|
6548f01efa | ||
|
|
518a4484d6 | ||
|
|
3535a25a59 | ||
|
|
06cc6741e6 | ||
|
|
ed1478e1ba | ||
|
|
854c05d673 | ||
|
|
bcf5fb18c5 | ||
|
|
3f822e814b | ||
|
|
4fd25c7215 | ||
|
|
f42bed2a55 | ||
|
|
469c205033 | ||
|
|
31551f3b9e | ||
|
|
69925af17c | ||
|
|
2502868ebc | ||
|
|
38d242add9 | ||
|
|
c272e281cf | ||
|
|
d4e19b0fa4 | ||
|
|
c12877a58f | ||
|
|
af83350d04 | ||
|
|
9d0cf6be0b | ||
|
|
2fe9c782d4 | ||
|
|
d0a3d8d144 | ||
|
|
cefcb1eaf8 | ||
|
|
83b60de640 | ||
|
|
a076418989 | ||
|
|
64ecce1ccb | ||
|
|
cd56a5d4f0 | ||
|
|
d5ca121ac6 | ||
|
|
1e20e80b65 | ||
|
|
75ac3fd40c | ||
|
|
3b91cc7f02 | ||
|
|
23450b61e0 | ||
|
|
dcc226976c | ||
|
|
196fde72f6 | ||
|
|
1e913cd406 | ||
|
|
02425702d1 | ||
|
|
2494655369 | ||
|
|
0e0a78c47e | ||
|
|
ed02c10f54 | ||
|
|
9c40f76184 | ||
|
|
8eda30d88c | ||
|
|
9de19d35a8 | ||
|
|
0770403ca5 | ||
|
|
1a45cb35c1 | ||
|
|
15cdff76d9 | ||
|
|
8c4eb4b5fe | ||
|
|
ae30aafafe | ||
|
|
3b74717591 | ||
|
|
7a6349b4c0 | ||
|
|
3b51fff88d | ||
|
+5 |
2a0b0be9d0 | ||
|
|
f373c9d818 | ||
|
|
540bf3a1cb | ||
|
|
2d9064e92f | ||
|
|
9487819ec1 | ||
|
|
49bf14b196 | ||
|
|
344f16d6cc | ||
|
|
ccbf3b1e54 | ||
|
|
dcf0fad35d | ||
|
|
a836800c69 | ||
|
|
b8c6ac43f7 | ||
|
|
f5e0c6b384 | ||
|
|
2a5dd9035f | ||
|
|
2cddad4336 | ||
|
|
4bcbff51b1 | ||
|
|
ae541e4c37 | ||
|
|
f254711d85 | ||
|
|
533e208643 | ||
|
|
795574ef88 | ||
|
|
48611d5c99 | ||
|
|
11a07961d0 | ||
|
|
6be0fd52e7 | ||
|
|
78da7ba338 | ||
|
|
9a72c648c5 | ||
|
|
3e42da7fc7 | ||
|
|
1d986cad04 | ||
|
|
f603e4eff2 | ||
|
|
1f4e6c1987 | ||
|
|
e57f734e79 | ||
|
|
7fad76ae23 | ||
|
|
7659a016b4 | ||
|
|
34fe1c4a95 | ||
|
|
205f73c4bc | ||
|
|
c4bd2167e1 | ||
|
|
49a7111b66 | ||
|
|
ca045a040b | ||
|
|
a8b4198bf7 | ||
|
|
2ab24e530b | ||
|
|
734b06dfdf | ||
|
|
467084993b | ||
|
|
1e2c8516d2 | ||
|
|
d57738f325 | ||
|
|
c39a7dba85 | ||
|
|
fd37397e5c | ||
|
|
df0a522656 | ||
|
|
916468b8cc | ||
|
|
b789e58624 | ||
|
|
4368536141 | ||
|
|
9eef9766ce | ||
|
|
68dbfbd7d0 | ||
|
|
692a84a86e | ||
|
|
1671095c94 |
@@ -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"
|
||||
}
|
||||
@@ -102,6 +102,8 @@ STRIPE_TEAM_MONTHLY_PRICE_ID=
|
||||
STRIPE_WEBHOOK_SECRET=
|
||||
STRIPE_PRIVATE_KEY=
|
||||
STRIPE_CLIENT_ID=
|
||||
PAYMENT_FEE_FIXED=
|
||||
PAYMENT_FEE_PERCENTAGE=
|
||||
|
||||
# Use for internal Public API Keys and optional
|
||||
API_KEY_PREFIX=cal_
|
||||
|
||||
@@ -3,3 +3,10 @@
|
||||
|
||||
automerge:
|
||||
- apps/web/public/static/locales/**/*.json
|
||||
|
||||
"❗️ migrations":
|
||||
- packages/prisma/migrations/**/migration.sql
|
||||
|
||||
"❗️ .env changes":
|
||||
- .env.example
|
||||
- .env.appStore.example
|
||||
|
||||
@@ -22,6 +22,7 @@ jobs:
|
||||
upload_translations: true
|
||||
download_translations: true
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||
# So required checks run on Crowdin PRs
|
||||
GITHUB_TOKEN: ${{ secrets.GH_ACCESS_TOKEN }}
|
||||
CROWDIN_PROJECT_ID: ${{ secrets.CROWDIN_PROJECT_ID }}
|
||||
CROWDIN_PERSONAL_TOKEN: ${{ secrets.CROWDIN_PERSONAL_TOKEN }}
|
||||
|
||||
@@ -12,8 +12,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/labeler@v4
|
||||
# Checking the actor will prevent your Action run failing on non-Dependabot
|
||||
# PRs but also ensures that it only does work for Dependabot PRs.
|
||||
if: ${{ github.actor == 'github-actions[bot]' }}
|
||||
with:
|
||||
repo-token: "${{ secrets.GITHUB_TOKEN }}"
|
||||
# https://github.com/actions/labeler/issues/442#issuecomment-1297359481
|
||||
sync-labels: ""
|
||||
|
||||
+1
-1
Submodule apps/api updated: 12f19ff7c0...7aebdb8c96
+1
-1
Submodule apps/console updated: ad226cef6e...8dd0779c43
@@ -17,7 +17,7 @@
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"iframe-resizer-react": "^1.1.0",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"nextra": "^1.1.0",
|
||||
"nextra-theme-docs": "^1.2.2",
|
||||
"react": "^18.2.0",
|
||||
|
||||
@@ -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";
|
||||
```
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ All apps can be found under `packages/app-store`. In this folder is `_example` w
|
||||
| ├──index.ts
|
||||
| ├──package.json
|
||||
| ├──.env.example
|
||||
| ├──README.mdx
|
||||
| ├──DESCRIPTION.md
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
@@ -48,9 +48,9 @@ Under the `/components` folder, this is where the install button for your app sh
|
||||
|
||||
The `/lib` folder is where the functions of your app live. For example, when creating a booking with a MS Teams link the function to make the call to grab the link lives in the `/lib` folder. Export your endpoints in an `index.ts` file under `/lib` folder and import them in your main `index.ts` file.
|
||||
|
||||
On the app store page you can customize your apps description by adding a markdown file called `README.mdx`. If you do not add one then the description from you `package.json` will be used instead.
|
||||
On the app store page you can customize your apps description by adding a markdown file called `DESCRIPTION.md`. If you do not add one then the description from you `package.json` will be used instead.
|
||||
|
||||
The `/static` folder is where you can store your app icon and any images that your `README.mdx` may use.
|
||||
The `/static` folder is where you can store your app icon and any images that your `DESCRIPTION.md` may use.
|
||||
|
||||
## Adding Your App to the App Store
|
||||
To render your app on the app store page, go to `packages/app-store/index.ts`. Import your app into the file and add it to the `appStore` object.
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"@radix-ui/react-slider": "^1.0.0",
|
||||
"@radix-ui/react-switch": "^1.0.0",
|
||||
"@radix-ui/react-tooltip": "^1.0.0",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0"
|
||||
},
|
||||
|
||||
@@ -189,3 +189,7 @@
|
||||
padding: 24px !important;
|
||||
}
|
||||
}
|
||||
|
||||
.docs-story {
|
||||
padding: 24px !important;
|
||||
}
|
||||
|
||||
@@ -14,7 +14,7 @@
|
||||
"dependencies": {
|
||||
"highlight.js": "^11.6.0",
|
||||
"isarray": "2.0.5",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"openapi-snippet": "^0.13.0",
|
||||
"react": "^18.2.0",
|
||||
"react-dom": "^18.2.0",
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
14.17
|
||||
v16
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
import { Suspense, SuspenseProps } from "react";
|
||||
|
||||
/**
|
||||
* Wrapper around `<Suspense />` which will render the `fallback` when on server
|
||||
* Can be simply replaced by `<Suspense />` once React 18 is ready.
|
||||
*/
|
||||
export const ClientSuspense = (props: SuspenseProps) => {
|
||||
return <>{typeof window !== "undefined" ? <Suspense {...props} /> : props.fallback}</>;
|
||||
};
|
||||
@@ -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) => {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -62,7 +62,7 @@ const NavTabs: FC<NavTabProps> = ({ tabs, linkProps, ...props }) => {
|
||||
const className = tab.className || "";
|
||||
return (
|
||||
<Component key={tab.name}>
|
||||
<Link key={tab.name} href={href} {...linkProps}>
|
||||
<Link key={tab.name} href={href} {...linkProps} legacyBehavior>
|
||||
<a
|
||||
onClick={onClick}
|
||||
className={classNames(
|
||||
|
||||
@@ -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,
|
||||
@@ -100,8 +98,10 @@ const Component = ({
|
||||
<h1 className="font-cal ml-4 text-3xl text-gray-900">{name}</h1>
|
||||
</div>
|
||||
<h2 className="text-sm font-medium text-gray-600">
|
||||
<Link href={`categories/${categories[0]}`}>
|
||||
<a className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">{categories[0]}</a>
|
||||
<Link
|
||||
href={`categories/${categories[0]}`}
|
||||
className="rounded-md bg-gray-100 p-1 text-xs capitalize text-gray-800">
|
||||
{categories[0]}
|
||||
</Link>{" "}
|
||||
• {t("published_by", { author })}
|
||||
</h2>
|
||||
|
||||
@@ -15,7 +15,7 @@ import {
|
||||
List,
|
||||
ShellSubHeading,
|
||||
showToast,
|
||||
SkeletonLoader,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
Switch,
|
||||
} from "@calcom/ui";
|
||||
|
||||
@@ -159,6 +159,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
if (!data.connectedCalendars.length) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
@@ -168,7 +169,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
slug={item.integration.slug}
|
||||
title={item.integration.title}
|
||||
logo={item.integration.logo}
|
||||
description={item.primary?.externalId || "No external Id"}
|
||||
description={item.primary?.email ?? item.integration.description}
|
||||
separate={true}
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
@@ -329,7 +330,10 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
})}
|
||||
description={t(`no_category_apps_description_calendar`)}
|
||||
buttonRaw={
|
||||
<Button color="secondary" href="/apps/categories/calendar">
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="connect-calendar-apps"
|
||||
href="/apps/categories/calendar">
|
||||
{t(`connect_calendar_apps`)}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -56,7 +56,7 @@ function IntegrationListItem(props: {
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
{/* Alert error that key stopped working. */}
|
||||
{props.invalidCredential && (
|
||||
<div className="flex items-center space-x-2">
|
||||
<div className="flex items-center space-x-2 rtl:space-x-reverse">
|
||||
<Icon.FiAlertCircle className="w-8 text-red-500 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
|
||||
+1
-5
@@ -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
-5
@@ -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)[] = [
|
||||
{
|
||||
+3
-3
@@ -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>
|
||||
);
|
||||
|
||||
@@ -57,7 +57,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||
</span>
|
||||
<span className="text-bookinglight font-medium">
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
||||
, {date.toDate().toLocaleString(i18n.language, { month: "short" })} {date.format(" D ")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="ml-auto">
|
||||
@@ -71,7 +71,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||
<div className="flex-grow overflow-y-auto sm:block md:h-[364px]">
|
||||
{slots.length > 0 &&
|
||||
slots.map((slot) => {
|
||||
type BookingURL = {
|
||||
@@ -101,7 +101,8 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
}
|
||||
|
||||
return (
|
||||
<div key={dayjs(slot.time).format()}>
|
||||
<div data-slot-owner={(slot.userIds || []).join(",")} key={`${dayjs(slot.time).format()}`}>
|
||||
{/* ^ data-slot-owner is helpful in debugging and used to identify the owners of the slot. Owners are the users which have the timeslot in their schedule. It doesn't consider if a user has that timeslot booked */}
|
||||
{/* Current there is no way to disable Next.js Links */}
|
||||
{seatsPerTimeSlot && slot.attendees && slot.attendees >= seatsPerTimeSlot ? (
|
||||
<div
|
||||
@@ -113,29 +114,29 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
{!!seatsPerTimeSlot && <p className="text-sm">{t("booking_full")}</p>}
|
||||
</div>
|
||||
) : (
|
||||
<Link href={bookingUrl} prefetch={false}>
|
||||
<a
|
||||
className={classNames(
|
||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</a>
|
||||
<Link
|
||||
href={bookingUrl}
|
||||
prefetch={false}
|
||||
className={classNames(
|
||||
"text-primary-500 hover:border-gray-900 hover:bg-gray-50",
|
||||
"dark:bg-darkgray-200 dark:hover:bg-darkgray-300 dark:hover:border-darkmodebrand mb-2 block rounded-md border bg-white py-2 text-sm font-medium dark:border-transparent dark:text-neutral-200",
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
data-testid="time">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
<p
|
||||
className={`${
|
||||
slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.8
|
||||
? "text-rose-600"
|
||||
: slot.attendees && slot.attendees / seatsPerTimeSlot >= 0.33
|
||||
? "text-yellow-500"
|
||||
: "text-emerald-400"
|
||||
} text-sm`}>
|
||||
{slot.attendees ? seatsPerTimeSlot - slot.attendees : seatsPerTimeSlot} /{" "}
|
||||
{seatsPerTimeSlot} {t("seats_available")}
|
||||
</p>
|
||||
)}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
|
||||
@@ -44,18 +44,17 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
const { profile, eventType, isBookingPage = false, children } = props;
|
||||
const { date: bookingDate } = useRouterQuery("date");
|
||||
const { t } = useLocale();
|
||||
const { duration, setQuery: setDuration } = useRouterQuery("duration");
|
||||
const { duration = eventType.length.toString(), setQuery: setDuration } = useRouterQuery("duration");
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.metadata?.multipleDuration !== undefined) {
|
||||
if (!duration) {
|
||||
setDuration(eventType.length);
|
||||
} else {
|
||||
if (!eventType.metadata?.multipleDuration.includes(Number(duration))) {
|
||||
setDuration(eventType.length);
|
||||
}
|
||||
}
|
||||
if (
|
||||
eventType.metadata?.multipleDuration &&
|
||||
!eventType.metadata?.multipleDuration?.includes(Number(duration))
|
||||
) {
|
||||
setDuration(eventType.length.toString());
|
||||
}
|
||||
}, []);
|
||||
}, [duration, setDuration, eventType.length, eventType.metadata?.multipleDuration]);
|
||||
|
||||
let requiresConfirmation = eventType?.requiresConfirmation;
|
||||
let requiresConfirmationText = t("requires_confirmation");
|
||||
const rcThreshold = eventType?.metadata?.requiresConfirmationThreshold;
|
||||
@@ -96,7 +95,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
<div>
|
||||
<Icon.FiInfo
|
||||
className={classNames(
|
||||
"mr-[10px] ml-[2px] inline-block h-4 w-4",
|
||||
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
||||
)}
|
||||
/>
|
||||
@@ -113,7 +112,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiCheckSquare className="mr-[10px] ml-[2px] inline-block h-4 w-4 " />
|
||||
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
</div>
|
||||
{requiresConfirmationText}
|
||||
</div>
|
||||
@@ -128,7 +127,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
)}>
|
||||
<Icon.FiClock
|
||||
className={classNames(
|
||||
"min-h-4 min-w-4 mr-[10px] ml-[2px] inline-block",
|
||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "mt-[2px]"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -4,6 +4,7 @@ import { useState } from "react";
|
||||
|
||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import ViewRecordingsDialog from "@calcom/features/ee/video/ViewRecordingsDialog";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { formatTime } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
@@ -22,15 +23,16 @@ import {
|
||||
showToast,
|
||||
TextArea,
|
||||
Tooltip,
|
||||
ActionType,
|
||||
TableActions,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["status"];
|
||||
type BookingListingStatus = RouterInputs["viewer"]["bookings"]["get"]["filters"]["status"];
|
||||
|
||||
type BookingItem = RouterOutputs["viewer"]["bookings"]["get"]["bookings"][number];
|
||||
|
||||
@@ -48,6 +50,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.status === BookingStatus.REJECTED) {
|
||||
@@ -112,6 +115,17 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
];
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
{
|
||||
id: "view_recordings",
|
||||
label: t("view_recordings"),
|
||||
onClick: () => {
|
||||
setViewRecordingsDialogIsOpen(true);
|
||||
},
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
];
|
||||
|
||||
let bookedActions: ActionType[] = [
|
||||
{
|
||||
id: "cancel",
|
||||
@@ -158,6 +172,10 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
|
||||
if (isPast && isPending && !isConfirmed) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "cancel");
|
||||
}
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
@@ -202,7 +220,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const showRecordingsButtons = booking.location === "integrations:daily" && isPast && isConfirmed;
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -216,7 +234,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isOpenDialog={isOpenSetLocationDialog}
|
||||
setShowLocationModal={setIsOpenLocationDialog}
|
||||
/>
|
||||
|
||||
{showRecordingsButtons && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
isOpenDialog={viewRecordingsDialogIsOpen}
|
||||
setIsOpenDialog={setViewRecordingsDialogIsOpen}
|
||||
timeFormat={user?.timeFormat ?? null}
|
||||
/>
|
||||
)}
|
||||
{/* NOTE: Should refactor this dialog component as is being rendered multiple times */}
|
||||
<Dialog open={rejectionDialogIsOpen} onOpenChange={setRejectionDialogIsOpen}>
|
||||
<DialogContent>
|
||||
@@ -248,7 +273,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||
<tr className="group flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||
<td
|
||||
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||
onClick={onClickTableData}>
|
||||
@@ -275,11 +300,6 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
{booking.eventType.team.name}
|
||||
</Badge>
|
||||
)}
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
{booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||
{t("paid")}
|
||||
@@ -343,12 +363,14 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<span> </span>
|
||||
|
||||
{!!booking?.eventType?.price && !booking.paid && (
|
||||
<Tag className="hidden ltr:ml-2 rtl:mr-2 sm:inline-flex">Pending payment</Tag>
|
||||
<Badge className="hidden ltr:ml-2 ltr:mr-2 rtl:ml-2 sm:inline-flex" variant="orange">
|
||||
{t("pending_payment")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
{booking.description && (
|
||||
<div
|
||||
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
||||
className="max-w-10/12 sm:max-w-32 md:max-w-52 xl:max-w-80 truncate text-sm text-gray-600"
|
||||
title={booking.description}>
|
||||
"{booking.description}"
|
||||
</div>
|
||||
@@ -376,6 +398,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
</>
|
||||
) : null}
|
||||
{isPast && isPending && !isConfirmed ? <TableActions actions={bookedActions} /> : null}
|
||||
{showRecordingsButtons && <TableActions actions={showRecordingActions} />}
|
||||
{isCancelled && booking.rescheduled && (
|
||||
<div className="hidden h-full items-center md:flex">
|
||||
<RequestSentMessage />
|
||||
@@ -531,13 +554,4 @@ const DisplayAttendees = ({
|
||||
);
|
||||
};
|
||||
|
||||
const Tag = ({ children, className = "" }: React.PropsWithChildren<{ className?: string }>) => {
|
||||
return (
|
||||
<span
|
||||
className={`inline-flex items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800 ${className}`}>
|
||||
{children}
|
||||
</span>
|
||||
);
|
||||
};
|
||||
|
||||
export default BookingListItem;
|
||||
|
||||
@@ -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]";
|
||||
@@ -151,7 +151,10 @@ const SlotPicker = ({
|
||||
eventTypeId: eventType.id,
|
||||
eventTypeSlug: eventType.slug,
|
||||
usernameList: users,
|
||||
startTime: browsingDate?.startOf("month"),
|
||||
startTime:
|
||||
browsingDate === undefined || browsingDate.get("month") === dayjs.tz(undefined, timeZone).get("month")
|
||||
? dayjs.tz(undefined, timeZone).subtract(2, "days").startOf("day")
|
||||
: browsingDate?.startOf("month"),
|
||||
endTime: browsingDate?.endOf("month"),
|
||||
timeZone,
|
||||
duration,
|
||||
@@ -217,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" />
|
||||
@@ -364,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 })}
|
||||
@@ -389,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}
|
||||
@@ -410,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>
|
||||
|
||||
@@ -143,6 +143,7 @@ const BookingPage = ({
|
||||
isSuccessBookingPage: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -158,6 +159,7 @@ const BookingPage = ({
|
||||
allRemainingBookings: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -299,6 +301,39 @@ const BookingPage = ({
|
||||
}
|
||||
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
const bookingCustomInputs = Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && booking.customInputs[inputId] ? booking.customInputs[inputId] : "",
|
||||
}));
|
||||
|
||||
// Checking if custom inputs of type Phone number are valid to display error message on UI
|
||||
if (eventType.customInputs.length) {
|
||||
let isErrorFound = false;
|
||||
eventType.customInputs.forEach((customInput) => {
|
||||
if (customInput.required && customInput.type === EventTypeCustomInputType.PHONE) {
|
||||
const input = bookingCustomInputs.find((i) => i.label === customInput.label);
|
||||
try {
|
||||
z.string({
|
||||
errorMap: () => ({
|
||||
message: `Missing ${customInput.type} customInput: '${customInput.label}'`,
|
||||
}),
|
||||
})
|
||||
.refine((val) => isValidPhoneNumber(val), {
|
||||
message: "Phone number is invalid",
|
||||
})
|
||||
.parse(input?.value);
|
||||
} catch (err) {
|
||||
isErrorFound = true;
|
||||
bookingForm.setError(`customInputs.${customInput.id}`, {
|
||||
type: "custom",
|
||||
message: "Invalid Phone number",
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
if (isErrorFound) return;
|
||||
}
|
||||
|
||||
telemetry.event(
|
||||
top !== window ? telemetryEventTypes.embedBookingConfirmed : telemetryEventTypes.bookingConfirmed,
|
||||
{ isTeamBooking: document.URL.includes("team/") }
|
||||
@@ -355,10 +390,7 @@ const BookingPage = ({
|
||||
attendeeAddress: booking.attendeeAddress,
|
||||
}),
|
||||
metadata,
|
||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
||||
})),
|
||||
customInputs: bookingCustomInputs,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
@@ -386,10 +418,7 @@ const BookingPage = ({
|
||||
attendeeAddress: booking.attendeeAddress,
|
||||
}),
|
||||
metadata,
|
||||
customInputs: Object.keys(booking.customInputs || {}).map((inputId) => ({
|
||||
label: eventType.customInputs.find((input) => input.id === parseInt(inputId))?.label || "",
|
||||
value: booking.customInputs && inputId in booking.customInputs ? booking.customInputs[inputId] : "",
|
||||
})),
|
||||
customInputs: bookingCustomInputs,
|
||||
hasHashedBookingLink,
|
||||
hashedLink,
|
||||
smsReminderNumber:
|
||||
@@ -472,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}
|
||||
@@ -484,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,
|
||||
@@ -495,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 &&
|
||||
@@ -523,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>
|
||||
@@ -531,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
|
||||
@@ -594,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>
|
||||
)}
|
||||
@@ -635,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>
|
||||
);
|
||||
@@ -660,7 +689,7 @@ const BookingPage = ({
|
||||
{selectedLocationType === LocationType.Phone
|
||||
? t("phone_number")
|
||||
: selectedLocationType === LocationType.AttendeeInPerson
|
||||
? t("Address")
|
||||
? t("address")
|
||||
: ""}
|
||||
</label>
|
||||
<div className="mt-1">
|
||||
@@ -687,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>
|
||||
)}
|
||||
@@ -771,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);
|
||||
@@ -779,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`}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
@@ -793,6 +823,23 @@ const BookingPage = ({
|
||||
</Group>
|
||||
</div>
|
||||
)}
|
||||
{input.type === EventTypeCustomInputType.PHONE && (
|
||||
<div>
|
||||
<PhoneInput<BookingFormValues>
|
||||
name={`customInputs.${input.id}`}
|
||||
control={bookingForm.control}
|
||||
placeholder={t("enter_phone_number")}
|
||||
id={`customInputs.${input.id}`}
|
||||
required={input.required}
|
||||
/>
|
||||
{bookingForm.formState.errors?.customInputs?.[input.id] && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!eventType.disableGuests && guestToggle && (
|
||||
@@ -867,7 +914,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
{bookingForm.formState.errors.smsReminderNumber && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<Icon.FiInfo className="mr-2 h-3 w-3" />
|
||||
<Icon.FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -354,7 +354,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
/>
|
||||
{selectedLocation && LocationOptions}
|
||||
<DialogFooter>
|
||||
<div className="mt-4 flex justify-end space-x-2">
|
||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
<Button
|
||||
onClick={() => {
|
||||
setShowLocationModal(false);
|
||||
|
||||
@@ -149,7 +149,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ml-4">-</span>
|
||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
@@ -164,7 +164,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<Icon.FiGlobe className="mr-2" />
|
||||
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
|
||||
@@ -40,6 +40,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
value: EventTypeCustomInputType.RADIO,
|
||||
label: t("radio"),
|
||||
},
|
||||
{ value: EventTypeCustomInputType.PHONE, label: t("phone_number") },
|
||||
];
|
||||
|
||||
const { selectedCustomInput } = props;
|
||||
@@ -133,8 +134,8 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
value={selectedCustomInput?.id || -1}
|
||||
{...register("id", { valueAsNumber: true })}
|
||||
/>
|
||||
<div className="mt-5 flex justify-end space-x-2 sm:mt-4">
|
||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2">
|
||||
<div className="mt-5 flex justify-end space-x-2 rtl:space-x-reverse sm:mt-4">
|
||||
<Button onClick={onCancel} type="button" color="secondary" className="ltr:mr-2 rtl:ml-2">
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
<Button
|
||||
|
||||
@@ -1,18 +1,19 @@
|
||||
import Link from "next/link";
|
||||
import type { CustomInputParsed, EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import type { CustomInputParsed, EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import {
|
||||
Badge,
|
||||
Button,
|
||||
Checkbox,
|
||||
CustomInputItem,
|
||||
Dialog,
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
@@ -36,7 +37,19 @@ const generateHashedLink = (id: number) => {
|
||||
return uid;
|
||||
};
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered, "eventType" | "team">) => {
|
||||
const getRandomId = (length = 8) => {
|
||||
return (
|
||||
-1 *
|
||||
parseInt(
|
||||
Math.ceil(Math.random() * Date.now())
|
||||
.toPrecision(length)
|
||||
.toString()
|
||||
.replace(".", "")
|
||||
)
|
||||
);
|
||||
};
|
||||
|
||||
export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const connectedCalendarsQuery = trpc.viewer.connectedCalendars.useQuery();
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
@@ -51,7 +64,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
|
||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
@@ -64,6 +76,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
}, [eventType.users, hashedUrl, team?.id]);
|
||||
|
||||
useEffect(() => {
|
||||
if (eventType.customInputs) {
|
||||
setCustomInputs(eventType.customInputs.sort((a, b) => a.id - b.id));
|
||||
}
|
||||
}, [eventType.customInputs]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col space-y-8">
|
||||
{/**
|
||||
@@ -75,10 +93,11 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
||||
<div className="flex flex-col">
|
||||
<div className="flex justify-between">
|
||||
<Label>{t("add_to_calendar")}</Label>
|
||||
<Link href="/apps/categories/calendar">
|
||||
<a target="_blank" className="text-sm text-gray-600 hover:text-gray-900">
|
||||
{t("add_another_calendar")}
|
||||
</a>
|
||||
<Link
|
||||
href="/apps/categories/calendar"
|
||||
target="_blank"
|
||||
className="text-sm text-gray-600 hover:text-gray-900">
|
||||
{t("add_another_calendar")}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="-mt-1 w-full">
|
||||
@@ -239,13 +258,28 @@ 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" target="_blank">
|
||||
<Badge variant="orange" className="ml-2">
|
||||
Platform Only
|
||||
</Badge>
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</SettingsToggle>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
<hr />
|
||||
|
||||
<SettingsToggle
|
||||
data-testid="hashedLinkCheck"
|
||||
title={t("private_link")}
|
||||
@@ -374,7 +408,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 +420,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 +435,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"> {period.suffix}</span> : null}
|
||||
{period.suffix ? (
|
||||
<span className="ltr:ml-2 ltr:mr-2 rtl:ml-2"> {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,16 +1,18 @@
|
||||
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 { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
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";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Label, Select, Skeleton, TextField, SettingsToggle } from "@calcom/ui";
|
||||
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
|
||||
import { slugify } from "@lib/slugify";
|
||||
|
||||
@@ -23,7 +25,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 +171,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">
|
||||
@@ -195,6 +197,23 @@ export const EventSetupTab = (
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
{validLocations.some((location) => location.type === MeetLocationType) && (
|
||||
<div className="flex text-sm text-gray-600">
|
||||
<Icon.FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||
<Trans i18nKey="event_type_requres_google_cal">
|
||||
<p>
|
||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||
Change it{" "}
|
||||
<Link
|
||||
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
|
||||
className="underline">
|
||||
here.
|
||||
</Link>{" "}
|
||||
We will fall back to Cal video if you do not change it.
|
||||
</p>
|
||||
</Trans>
|
||||
</div>
|
||||
)}
|
||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||
<li>
|
||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
@@ -262,12 +281,14 @@ export const EventSetupTab = (
|
||||
if (!newOptions.find((opt) => opt.value === defaultDuration?.value)) {
|
||||
if (newOptions.length > 0) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
} else {
|
||||
setDefaultDuration(null);
|
||||
}
|
||||
}
|
||||
if (newOptions.length === 1 && defaultDuration === null) {
|
||||
setDefaultDuration(newOptions[0]);
|
||||
formMethods.setValue("length", newOptions[0].value);
|
||||
}
|
||||
formMethods.setValue("metadata.multipleDuration", values);
|
||||
}}
|
||||
@@ -288,7 +309,7 @@ export const EventSetupTab = (
|
||||
setDefaultDuration(
|
||||
selectedMultipleDuration.find((opt) => opt.value === option?.value) ?? null
|
||||
);
|
||||
formMethods.setValue("length", Number(option?.value));
|
||||
if (option) formMethods.setValue("length", option.value);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
@@ -296,14 +317,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">
|
||||
@@ -327,6 +345,7 @@ export const EventSetupTab = (
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
{t("location")}
|
||||
</Skeleton>
|
||||
|
||||
<Controller
|
||||
name="locations"
|
||||
control={formMethods.control}
|
||||
@@ -343,7 +362,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}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { SchedulingType } from "@prisma/client/";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
|
||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Avatar, Button, CheckedTeamSelect, Icon, Label, Select } from "@calcom/ui";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
|
||||
interface IMemberToValue {
|
||||
id: number | null;
|
||||
@@ -25,8 +26,7 @@ export const EventTeamTab = ({
|
||||
eventType,
|
||||
team,
|
||||
teamMembers,
|
||||
currentUserMembership,
|
||||
}: Pick<EventTypeSetupInfered, "eventType" | "teamMembers" | "team" | "currentUserMembership">) => {
|
||||
}: Pick<EventTypeSetupProps, "eventType" | "teamMembers" | "team">) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -101,48 +101,3 @@ export const EventTeamTab = ({
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const TeamProfileCard = ({
|
||||
id,
|
||||
name,
|
||||
hasPermsToDelete,
|
||||
email,
|
||||
username,
|
||||
}: {
|
||||
id: number;
|
||||
name: string | null;
|
||||
email: string;
|
||||
username: string | null;
|
||||
hasPermsToDelete: boolean;
|
||||
}) => {
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
const avatar = `${WEBAPP_URL}/${username}/avatar.png`;
|
||||
|
||||
return (
|
||||
<div className="flex items-center border border-gray-300 px-6 py-4 first:rounded-t-md last:rounded-b-md only:rounded-md">
|
||||
<Avatar imageSrc={avatar} gravatarFallbackMd5="ajsdkwakhsd1231" size="md" alt="Team Image" />
|
||||
<div className="flex flex-col pl-3">
|
||||
<div className="">
|
||||
<span className="pr-2 text-sm font-semibold leading-none text-black">{name}</span>
|
||||
</div>
|
||||
<p className="text-sm font-normal leading-normal text-gray-700">{email}</p>
|
||||
</div>
|
||||
{hasPermsToDelete && (
|
||||
<div className="ml-auto">
|
||||
<Button
|
||||
type="button"
|
||||
size="icon"
|
||||
color="destructive"
|
||||
StartIcon={Icon.FiTrash}
|
||||
onClick={() => {
|
||||
formMethods.setValue(
|
||||
"users",
|
||||
formMethods.getValues("users").filter((user) => user !== id.toString())
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -0,0 +1,179 @@
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { TbWebhook } from "react-icons/tb";
|
||||
|
||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||
import { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Webhook } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogContent, EmptyScreen, Icon, showToast } from "@calcom/ui";
|
||||
|
||||
export const EventTeamWebhooksTab = ({
|
||||
eventType,
|
||||
team,
|
||||
}: Pick<EventTypeSetupProps, "eventType" | "team">) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const utils = trpc.useContext();
|
||||
|
||||
const { data: webhooks } = trpc.viewer.webhook.list.useQuery({ eventTypeId: eventType.id });
|
||||
|
||||
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
||||
variant: "other",
|
||||
onlyInstalled: true,
|
||||
});
|
||||
|
||||
const [createModalOpen, setCreateModalOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [webhookToEdit, setWebhookToEdit] = useState<Webhook>();
|
||||
|
||||
const subscriberUrlReserved = (subscriberUrl: string, id?: string): boolean => {
|
||||
return !!webhooks?.find(
|
||||
(webhook) => webhook.subscriberUrl === subscriberUrl && (!id || webhook.id !== id)
|
||||
);
|
||||
};
|
||||
|
||||
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||
async onSuccess() {
|
||||
setEditModalOpen(false);
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
showToast(t("webhook_updated_successfully"), "success");
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||
async onSuccess() {
|
||||
showToast(t("webhook_created_successfully"), "success");
|
||||
await utils.viewer.webhook.list.invalidate();
|
||||
setCreateModalOpen(false);
|
||||
},
|
||||
onError(error) {
|
||||
showToast(`${error.message}`, "error");
|
||||
},
|
||||
});
|
||||
|
||||
const onCreateWebhook = async (values: WebhookFormSubmitData) => {
|
||||
if (subscriberUrlReserved(values.subscriberUrl, values.id)) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
createWebhookMutation.mutate({
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: eventType.id,
|
||||
});
|
||||
};
|
||||
|
||||
const NewWebhookButton = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={Icon.FiPlus}
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
return (
|
||||
<div>
|
||||
{team && webhooks && !isLoading && (
|
||||
<>
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="mt-4 mb-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<NewWebhookButton />
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={TbWebhook}
|
||||
headline={t("create_your_first_webhook")}
|
||||
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||
buttonRaw={<NewWebhookButton />}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* New webhook dialog */}
|
||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
|
||||
<WebhookForm
|
||||
onSubmit={onCreateWebhook}
|
||||
onCancel={() => setCreateModalOpen(false)}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
{/* Edit webhook dialog */}
|
||||
<Dialog open={editModalOpen} onOpenChange={(isOpen) => !isOpen && setEditModalOpen(false)}>
|
||||
<DialogContent title={t("edit_webhook")}>
|
||||
<WebhookForm
|
||||
webhook={webhookToEdit}
|
||||
apps={installedApps?.items.map((app) => app.slug)}
|
||||
onCancel={() => setEditModalOpen(false)}
|
||||
onSubmit={(values: WebhookFormSubmitData) => {
|
||||
if (subscriberUrlReserved(values.subscriberUrl, webhookToEdit?.id || "")) {
|
||||
showToast(t("webhook_subscriber_url_reserved"), "error");
|
||||
return;
|
||||
}
|
||||
|
||||
if (values.changeSecret) {
|
||||
values.secret = values.newSecret.length ? values.newSecret : null;
|
||||
}
|
||||
|
||||
if (!values.payloadTemplate) {
|
||||
values.payloadTemplate = null;
|
||||
}
|
||||
|
||||
editWebhookMutation.mutate({
|
||||
id: webhookToEdit?.id || "",
|
||||
subscriberUrl: values.subscriberUrl,
|
||||
eventTriggers: values.eventTriggers,
|
||||
active: values.active,
|
||||
payloadTemplate: values.payloadTemplate,
|
||||
secret: values.secret,
|
||||
eventTypeId: webhookToEdit?.eventTypeId || undefined,
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -1,9 +1,9 @@
|
||||
import { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useState } from "react";
|
||||
import { Loader } from "react-feather";
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } 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";
|
||||
@@ -32,14 +32,13 @@ import {
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
|
||||
import { ClientSuspense } from "@components/ClientSuspense";
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
|
||||
type Props = {
|
||||
children: React.ReactNode;
|
||||
eventType: EventTypeSetupInfered["eventType"];
|
||||
currentUserMembership: EventTypeSetupInfered["currentUserMembership"];
|
||||
team: EventTypeSetupInfered["team"];
|
||||
eventType: EventTypeSetupProps["eventType"];
|
||||
currentUserMembership: EventTypeSetupProps["currentUserMembership"];
|
||||
team: EventTypeSetupProps["team"];
|
||||
disableBorder?: boolean;
|
||||
enabledAppsNumber: number;
|
||||
installedAppsNumber: number;
|
||||
@@ -150,13 +149,20 @@ function EventTypeSingleLayout({
|
||||
enabledWorkflowsNumber,
|
||||
});
|
||||
// If there is a team put this navigation item within the tabs
|
||||
if (team)
|
||||
if (team) {
|
||||
navigation.splice(2, 0, {
|
||||
name: "scheduling_type",
|
||||
href: `/event-types/${eventType.id}?tabName=team`,
|
||||
icon: Icon.FiUsers,
|
||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||
});
|
||||
navigation.push({
|
||||
name: "webhooks",
|
||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||
icon: TbWebhook,
|
||||
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
||||
});
|
||||
}
|
||||
return navigation;
|
||||
}, [t, eventType, installedAppsNumber, enabledAppsNumber, enabledWorkflowsNumber, team]);
|
||||
|
||||
@@ -294,7 +300,7 @@ function EventTypeSingleLayout({
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<ClientSuspense fallback={<Loader />}>
|
||||
<Suspense fallback={<Icon.FiLoader />}>
|
||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
@@ -317,7 +323,7 @@ function EventTypeSingleLayout({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ClientSuspense>
|
||||
</Suspense>
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
|
||||
@@ -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,
|
||||
})}
|
||||
|
||||
+1
-1
@@ -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,
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
|
||||
@@ -34,14 +34,8 @@ export const AvatarGroup = function AvatarGroup(props: AvatarGroupProps) {
|
||||
);
|
||||
|
||||
return (
|
||||
<li key={idx} className="-mr-2 inline-block">
|
||||
{item.href ? (
|
||||
<Link href={item.href}>
|
||||
<a>{avatar}</a>
|
||||
</Link>
|
||||
) : (
|
||||
avatar
|
||||
)}
|
||||
<li key={idx} className="-ltr:mr-2 inline-block rtl:ml-2">
|
||||
{item.href ? <Link href={item.href}>{avatar}</Link> : avatar}
|
||||
</li>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,22 +0,0 @@
|
||||
import Link from "next/link";
|
||||
|
||||
interface ILinkTextProps {
|
||||
href: string;
|
||||
children: React.ReactNode;
|
||||
classNameChildren?: string;
|
||||
target?: string;
|
||||
}
|
||||
/**
|
||||
* This component had to be made in order to make i18n work with next/link
|
||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
||||
**/
|
||||
export const LinkText = (props: ILinkTextProps) => {
|
||||
const { target, href, children, classNameChildren, ...moreProps } = props;
|
||||
return (
|
||||
<Link href={href || ""} {...moreProps}>
|
||||
<a target={target || ""} className={classNameChildren}>
|
||||
{children}
|
||||
</a>
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -9,26 +9,21 @@ const PoweredByCal = () => {
|
||||
const isEmbed = useIsEmbed();
|
||||
return (
|
||||
<div className={"p-2 text-center text-xs sm:text-right" + (isEmbed ? " max-w-3xl" : "")}>
|
||||
<Link href={POWERED_BY_URL}>
|
||||
<a target="_blank" className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||
{t("powered_by")}{" "}
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||
src="/cal-logo-word.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
}
|
||||
{
|
||||
// eslint-disable-next-line @next/next/no-img-element
|
||||
<img
|
||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||
src="/cal-logo-word-dark.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
}
|
||||
</a>
|
||||
<Link
|
||||
href={POWERED_BY_URL}
|
||||
target="_blank"
|
||||
className="text-bookinglight opacity-50 hover:opacity-100 dark:text-white">
|
||||
{t("powered_by")}{" "}
|
||||
<img
|
||||
className="relative -mt-px inline h-[10px] w-auto dark:hidden"
|
||||
src="/cal-logo-word.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
<img
|
||||
className="relativ -mt-px hidden h-[10px] w-auto dark:inline"
|
||||
src="/cal-logo-word-dark.svg"
|
||||
alt="Cal.com Logo"
|
||||
/>
|
||||
</Link>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -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",
|
||||
@@ -197,7 +187,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
<div className="flex justify-items-center">
|
||||
<Label htmlFor="username">{t("username")}</Label>
|
||||
</div>
|
||||
<div className="mt-2 flex rounded-md">
|
||||
<div className="flex rounded-md">
|
||||
<span
|
||||
className={classNames(
|
||||
isInputUsernamePremium ? "border-1 border-orange-400 " : "",
|
||||
@@ -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="flex rounded-md">
|
||||
<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">
|
||||
|
||||
@@ -109,7 +109,7 @@ function Select<
|
||||
IndicatorSeparator: () => null,
|
||||
Input: InputComponent,
|
||||
}}
|
||||
className={classNames("text-sm", className)}
|
||||
className={classNames("border-0 text-sm", className)}
|
||||
{...props}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -1,79 +0,0 @@
|
||||
import React, { ReactNode, useState } from "react";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
type RadioAreaProps = Omit<React.InputHTMLAttributes<HTMLInputElement>, "onChange"> & {
|
||||
onChange?: (value: string) => void;
|
||||
defaultChecked?: boolean;
|
||||
};
|
||||
|
||||
const RadioArea = (props: RadioAreaProps) => {
|
||||
return (
|
||||
<label
|
||||
className={classNames(
|
||||
"border-1 block border p-4 focus:outline-none focus:ring focus:ring-neutral-500",
|
||||
props.checked && "border-brand",
|
||||
props.className
|
||||
)}>
|
||||
<input
|
||||
onChange={(e) => {
|
||||
if (typeof props.onChange === "function") {
|
||||
props.onChange(e.target.value);
|
||||
}
|
||||
}}
|
||||
checked={props.checked}
|
||||
className="float-right ml-3 text-neutral-900 focus:ring-neutral-500"
|
||||
name={props.name}
|
||||
value={props.value}
|
||||
type="radio"
|
||||
/>
|
||||
{props.children}
|
||||
</label>
|
||||
);
|
||||
};
|
||||
|
||||
type ChildrenProps = {
|
||||
props: RadioAreaProps;
|
||||
children?: ReactNode;
|
||||
};
|
||||
interface RadioAreaGroupProps extends Omit<React.ComponentPropsWithoutRef<"div">, "onChange" | "children"> {
|
||||
children: ChildrenProps | ChildrenProps[];
|
||||
name?: string;
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
const RadioAreaGroup = ({ children, name, onChange, ...passThroughProps }: RadioAreaGroupProps) => {
|
||||
const [checkedIdx, setCheckedIdx] = useState<number | null>(null);
|
||||
|
||||
const changeHandler = (value: string, idx: number) => {
|
||||
if (onChange) {
|
||||
onChange(value);
|
||||
}
|
||||
setCheckedIdx(idx);
|
||||
};
|
||||
|
||||
return (
|
||||
<div {...passThroughProps}>
|
||||
{(Array.isArray(children) ? children : [children]).map((child, idx: number) => {
|
||||
if (checkedIdx === null && child.props.defaultChecked) {
|
||||
setCheckedIdx(idx);
|
||||
}
|
||||
return (
|
||||
<Item
|
||||
{...child.props}
|
||||
key={idx}
|
||||
name={name}
|
||||
checked={idx === checkedIdx}
|
||||
onChange={(value: string) => changeHandler(value, idx)}>
|
||||
{child.props.children}
|
||||
</Item>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const Item = RadioArea;
|
||||
const Group = RadioAreaGroup;
|
||||
|
||||
export { RadioArea, RadioAreaGroup, Item, Group };
|
||||
@@ -1,61 +0,0 @@
|
||||
import { Collapsible, CollapsibleContent, CollapsibleTrigger } from "@radix-ui/react-collapsible";
|
||||
import React from "react";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
import { RadioArea, RadioAreaGroup } from "@components/ui/form/radio-area/RadioAreaGroup";
|
||||
|
||||
interface OptionProps
|
||||
extends Pick<React.OptionHTMLAttributes<HTMLOptionElement>, "value" | "label" | "className"> {
|
||||
description?: string;
|
||||
}
|
||||
|
||||
interface RadioAreaSelectProps extends Omit<React.SelectHTMLAttributes<HTMLSelectElement>, "onChange"> {
|
||||
options: OptionProps[]; // allow options to be passed programmatically, like options={}
|
||||
onChange?: (value: string) => void;
|
||||
}
|
||||
|
||||
export const Select = function RadioAreaSelect(props: RadioAreaSelectProps) {
|
||||
const { t } = useLocale();
|
||||
const {
|
||||
options,
|
||||
disabled = !options.length, // if not explicitly disabled and the options length is empty, disable anyway
|
||||
placeholder = t("select"),
|
||||
} = props;
|
||||
|
||||
const getLabel = (value: string | ReadonlyArray<string> | number | undefined) =>
|
||||
options.find((option: OptionProps) => option.value === value)?.label;
|
||||
|
||||
return (
|
||||
<Collapsible className={classNames("w-full", props.className)}>
|
||||
<CollapsibleTrigger
|
||||
type="button"
|
||||
disabled={disabled}
|
||||
className={classNames(
|
||||
"border-1 focus:ring-primary-500 mb-1 block w-full cursor-pointer rounded-sm border border-gray-300 bg-white p-2 text-left text-sm",
|
||||
disabled && "cursor-default bg-gray-200 focus:ring-0 "
|
||||
)}>
|
||||
{getLabel(props.value) ?? placeholder}
|
||||
<Icon.FiChevronDown className="float-right h-5 w-5 text-neutral-500" />
|
||||
</CollapsibleTrigger>
|
||||
<CollapsibleContent>
|
||||
<RadioAreaGroup className="space-y-2 text-sm" name={props.name} onChange={props.onChange}>
|
||||
{options.map((option) => (
|
||||
<RadioArea
|
||||
{...option}
|
||||
key={Array.isArray(option.value) ? option.value.join(",") : `${option.value}`}
|
||||
defaultChecked={props.value === option.value}>
|
||||
<strong className="mb-1 block">{option.label}</strong>
|
||||
<p>{option.description}</p>
|
||||
</RadioArea>
|
||||
))}
|
||||
</RadioAreaGroup>
|
||||
</CollapsibleContent>
|
||||
</Collapsible>
|
||||
);
|
||||
};
|
||||
|
||||
export default Select;
|
||||
@@ -1,2 +0,0 @@
|
||||
export * from "./RadioAreaGroup";
|
||||
export * from "./Select";
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -89,6 +89,12 @@ const nextConfig = {
|
||||
eslint: {
|
||||
ignoreDuringBuilds: !!process.env.CI,
|
||||
},
|
||||
// TODO: We need to have all components in `@calcom/ui/components` in order to use this
|
||||
// modularizeImports: {
|
||||
// "@calcom/ui": {
|
||||
// transform: "@calcom/ui/components/{{member}}",
|
||||
// },
|
||||
// },
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
@@ -227,6 +233,11 @@ const nextConfig = {
|
||||
destination: "/404",
|
||||
permanent: false,
|
||||
},
|
||||
{
|
||||
source: "/booking/direct/:action/:email/:bookingUid/:oldToken",
|
||||
destination: "/api/link?action=:action&email=:email&bookingUid=:bookingUid&oldToken=:oldToken",
|
||||
permanent: true,
|
||||
},
|
||||
];
|
||||
|
||||
if (process.env.NEXT_PUBLIC_WEBAPP_URL === "https://app.cal.com") {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.4.0",
|
||||
"version": "2.4.6",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -67,6 +67,7 @@
|
||||
"bcryptjs": "^2.4.3",
|
||||
"classnames": "^2.3.1",
|
||||
"dotenv-cli": "^6.0.0",
|
||||
"eslint-config-next": "^13.1.1",
|
||||
"googleapis": "^84.0.0",
|
||||
"gray-matter": "^4.0.3",
|
||||
"handlebars": "^4.7.7",
|
||||
@@ -80,15 +81,14 @@
|
||||
"memory-cache": "^0.2.0",
|
||||
"micro": "^9.4.1",
|
||||
"mime-types": "^2.1.35",
|
||||
"next": "^12.3.1",
|
||||
"next": "^13.1.1",
|
||||
"next-auth": "^4.10.3",
|
||||
"next-axiom": "^0.10.0",
|
||||
"next-axiom": "^0.16.0",
|
||||
"next-collect": "^0.2.1",
|
||||
"next-i18next": "^11.3.0",
|
||||
"next-mdx-remote": "^4.1.0",
|
||||
"next-seo": "^4.26.0",
|
||||
"next-themes": "^0.2.0",
|
||||
"next-transpile-modules": "^9.0.0",
|
||||
"next-transpile-modules": "^10.0.0",
|
||||
"nock": "^13.2.8",
|
||||
"nodemailer": "^6.7.8",
|
||||
"otplib": "^12.0.1",
|
||||
|
||||
+47
-51
@@ -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() {
|
||||
@@ -100,26 +98,26 @@ export default function Custom404() {
|
||||
|
||||
<ul role="list" className="divide-y divide-gray-200 border-gray-200">
|
||||
<li className="px-4 py-2">
|
||||
<Link href={`${DEVELOPER_DOCS}/self-hosting/installation`}>
|
||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||
<Link
|
||||
href={`${DEVELOPER_DOCS}/self-hosting/installation`}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||
<Icon.FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-medium text-gray-900">
|
||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("prisma_studio_tip")}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-medium text-gray-900">
|
||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{t("prisma_studio_tip")}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</h3>
|
||||
<p className="text-base text-gray-500">{t("prisma_studio_tip_description")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
<li className="px-4 py-2">
|
||||
@@ -169,11 +167,11 @@ export default function Custom404() {
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
<Link href={`${WEBSITE_URL}/enterprise`}>
|
||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("contact_sales")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</a>
|
||||
<Link
|
||||
href={`${WEBSITE_URL}/enterprise`}
|
||||
className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("contact_sales")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -245,26 +243,26 @@ export default function Custom404() {
|
||||
<ul role="list" className="mt-4 divide-y divide-gray-200 border-gray-200">
|
||||
{links.map((link, linkIdx) => (
|
||||
<li key={linkIdx} className="px-4 py-2">
|
||||
<Link href={link.href}>
|
||||
<a className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||
<Link
|
||||
href={link.href}
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-gray-50">
|
||||
<link.icon className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-medium text-gray-900">
|
||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
<h3 className="text-base font-medium text-gray-900">
|
||||
<span className="rounded-sm focus-within:ring-2 focus-within:ring-gray-500 focus-within:ring-offset-2">
|
||||
<span className="absolute inset-0" aria-hidden="true" />
|
||||
{link.title}
|
||||
</span>
|
||||
</h3>
|
||||
<p className="text-base text-gray-500">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</h3>
|
||||
<p className="text-base text-gray-500">{link.description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -315,11 +313,9 @@ export default function Custom404() {
|
||||
</li>
|
||||
</ul>
|
||||
<div className="mt-8">
|
||||
<Link href="/">
|
||||
<a className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</a>
|
||||
<Link href="/" className="text-base font-medium text-black hover:text-gray-500">
|
||||
{t("or_go_back_home")}
|
||||
<span aria-hidden="true"> →</span>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
+38
-40
@@ -1,6 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import dynamic from "next/dynamic";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
@@ -12,6 +11,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 +26,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 +36,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
|
||||
@@ -58,27 +57,28 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
{eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="dark:bg-darkgray-100 dark:border-darkgray-200 group relative rounded-sm border border-neutral-200 bg-white dark:hover:border-neutral-600">
|
||||
className="dark:bg-darkgray-100 group relative border-b border-neutral-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-neutral-700 dark:hover:border-neutral-600">
|
||||
<Icon.FiArrowRight className="absolute right-3 top-3 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
<Link href={getUsernameSlugLink({ users: props.users, slug: type.slug })}>
|
||||
<a className="flex justify-between px-6 py-4" data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
border="border-2 border-white"
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size={10}
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</a>
|
||||
<Link
|
||||
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||
className="flex justify-between px-6 py-4"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex-shrink">
|
||||
<p className="dark:text-darkgray-700 text-sm font-semibold text-neutral-900">{type.title}</p>
|
||||
<EventTypeDescription className="text-sm" eventType={type} />
|
||||
</div>
|
||||
<div className="mt-1 self-center">
|
||||
<AvatarGroup
|
||||
border="border-2 border-white dark:border-darkgray-100"
|
||||
truncateAfter={4}
|
||||
className="flex flex-shrink-0"
|
||||
size={10}
|
||||
items={props.users.map((user) => ({
|
||||
alt: user.name || "",
|
||||
image: user.avatar || "",
|
||||
}))}
|
||||
/>
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
))}
|
||||
@@ -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>
|
||||
@@ -175,22 +175,20 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
href={{
|
||||
pathname: `/${user.username}/${type.slug}`,
|
||||
query,
|
||||
}}>
|
||||
<a
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-5"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</a>
|
||||
}}
|
||||
onClick={async () => {
|
||||
sdkActionManager?.fire("eventTypeSelected", {
|
||||
eventType: type,
|
||||
});
|
||||
}}
|
||||
className="block w-full p-5"
|
||||
data-testid="event-type-link">
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className="dark:text-darkgray-700 pr-2 text-sm font-semibold text-gray-700">
|
||||
{type.title}
|
||||
</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
</div>
|
||||
))
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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,11 +49,10 @@ 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>
|
||||
<link rel="preload" href="/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
|
||||
@@ -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) }),
|
||||
});
|
||||
@@ -35,6 +35,24 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
uid: "xxyPr4cg2xx4XoS2KeMEQy",
|
||||
metadata: {},
|
||||
recurringEvent: null,
|
||||
appsStatus: [
|
||||
{
|
||||
appName: "Outlook Calendar",
|
||||
type: "office365_calendar",
|
||||
success: 1,
|
||||
failures: 0,
|
||||
errors: [],
|
||||
warnings: [],
|
||||
},
|
||||
{
|
||||
appName: "Google Meet",
|
||||
type: "conferencing",
|
||||
success: 0,
|
||||
failures: 1,
|
||||
errors: [],
|
||||
warnings: ["In order to use Google Meet you must set your destination calendar to a Google Calendar"],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
req.statusCode = 200;
|
||||
@@ -42,9 +60,9 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
||||
res.setHeader("Content-Type", "text/html");
|
||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||
res.write(
|
||||
renderEmail("OrganizerRequestEmail", {
|
||||
attendee: evt.attendees[0],
|
||||
renderEmail("OrganizerScheduledEmail", {
|
||||
calEvent: evt,
|
||||
attendee: evt.organizer,
|
||||
})
|
||||
);
|
||||
res.end();
|
||||
|
||||
@@ -2,11 +2,11 @@ import { NextApiRequest, NextApiResponse } from "next";
|
||||
import type { Session } from "next-auth";
|
||||
|
||||
import getInstalledAppPath from "@calcom/app-store/_utils/getInstalledAppPath";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { deriveAppDictKeyFromType } from "@calcom/lib/deriveAppDictKeyFromType";
|
||||
import prisma from "@calcom/prisma";
|
||||
import type { AppDeclarativeHandler, AppHandler } from "@calcom/types/AppHandler";
|
||||
|
||||
import { getSession } from "@lib/auth";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
const defaultIntegrationAddHandler = async ({
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
import { UserPermissionRole } from "@prisma/client";
|
||||
import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import { z } from "zod";
|
||||
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultResponder } from "@calcom/lib/server";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import { createContext } from "@calcom/trpc/server/createContext";
|
||||
import { viewerRouter } from "@calcom/trpc/server/routers/viewer";
|
||||
|
||||
enum DirectAction {
|
||||
ACCEPT = "accept",
|
||||
REJECT = "reject",
|
||||
}
|
||||
|
||||
const querySchema = z.object({
|
||||
action: z.nativeEnum(DirectAction),
|
||||
token: z.string(),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
const decryptedSchema = z.object({
|
||||
bookingUid: z.string(),
|
||||
userId: z.number().int(),
|
||||
});
|
||||
|
||||
async function handler(req: NextApiRequest, res: NextApiResponse<Response>) {
|
||||
const { action, token, reason } = querySchema.parse(req.query);
|
||||
const { bookingUid, userId } = decryptedSchema.parse(
|
||||
JSON.parse(symmetricDecrypt(decodeURIComponent(token), process.env.CALENDSO_ENCRYPTION_KEY || ""))
|
||||
);
|
||||
|
||||
/** We shape the session as required by tRPC router */
|
||||
async function sessionGetter() {
|
||||
return {
|
||||
user: {
|
||||
id: userId,
|
||||
username: "" /* Not used in this context */,
|
||||
role: UserPermissionRole.USER,
|
||||
},
|
||||
hasValidLicense: true,
|
||||
expires: "" /* Not used in this context */,
|
||||
};
|
||||
}
|
||||
|
||||
const booking = await prisma.booking.findUniqueOrThrow({
|
||||
where: { uid: bookingUid },
|
||||
});
|
||||
|
||||
try {
|
||||
/** @see https://trpc.io/docs/server-side-calls */
|
||||
const ctx = await createContext({ req, res }, sessionGetter);
|
||||
const caller = viewerRouter.createCaller(ctx);
|
||||
await caller.bookings.confirm({
|
||||
bookingId: booking.id,
|
||||
recurringEventId: booking.recurringEventId || undefined,
|
||||
confirmed: action === DirectAction.ACCEPT,
|
||||
reason,
|
||||
});
|
||||
} catch (e) {
|
||||
let message = "Error confirming booking";
|
||||
if (e instanceof TRPCError) message = e.message;
|
||||
res.redirect(`/booking/${bookingUid}?error=${encodeURIComponent(message)}`);
|
||||
return;
|
||||
}
|
||||
|
||||
res.redirect(`/booking/${bookingUid}`);
|
||||
}
|
||||
|
||||
export default defaultResponder(handler);
|
||||
@@ -1,34 +1,18 @@
|
||||
import fs from "fs";
|
||||
import matter from "gray-matter";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import { MDXRemote } from "next-mdx-remote";
|
||||
import { serialize } from "next-mdx-remote/serialize";
|
||||
import Image from "next/image";
|
||||
import Link from "next/link";
|
||||
import path from "path";
|
||||
|
||||
import { getAppWithMetadata } from "@calcom/app-store/_appRegistry";
|
||||
import ExisitingGoogleCal from "@calcom/app-store/googlevideo/components/ExistingGoogleCal";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import App from "@components/apps/App";
|
||||
|
||||
const components = {
|
||||
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
|
||||
<Link href={href}>
|
||||
<a {...otherProps} />
|
||||
</Link>
|
||||
),
|
||||
img: ({ src = "", alt = "", placeholder, ...rest }: JSX.IntrinsicElements["img"]) => (
|
||||
<Image src={src} alt={alt} {...rest} />
|
||||
),
|
||||
// @TODO: In v2 the slider isn't shown anymore. However, to ensure the v1 pages keep
|
||||
// working, this component is still rendered in the MDX content. To skip them in the v2
|
||||
// content we have to render null here. In v2 the gallery is shown by directly
|
||||
// using the `items` property from the MDX's meta data.
|
||||
Slider: () => <></>,
|
||||
};
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
|
||||
function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
return (
|
||||
@@ -50,10 +34,15 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
||||
email={data.email}
|
||||
licenseRequired={data.licenseRequired}
|
||||
isProOnly={data.isProOnly}
|
||||
images={source?.scope?.items as string[] | undefined}
|
||||
images={source.data?.items as string[] | undefined}
|
||||
// tos="https://zoom.us/terms"
|
||||
// privacy="https://zoom.us/privacy"
|
||||
body={<MDXRemote {...source} components={components} />}
|
||||
body={
|
||||
<>
|
||||
{data.slug === "google-meet" && <ExisitingGoogleCal />}
|
||||
<div dangerouslySetInnerHTML={{ __html: md.render(source.content) }} />
|
||||
</>
|
||||
}
|
||||
/>
|
||||
);
|
||||
}
|
||||
@@ -82,7 +71,7 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
if (!singleApp) return { notFound: true };
|
||||
|
||||
const appDirname = app.dirName;
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/README.mdx`);
|
||||
const README_PATH = path.join(process.cwd(), "..", "..", `packages/app-store/${appDirname}/DESCRIPTION.md`);
|
||||
const postFilePath = path.join(README_PATH);
|
||||
let source = "";
|
||||
|
||||
@@ -90,16 +79,15 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
||||
/* If the app doesn't have a README we fallback to the package description */
|
||||
source = fs.readFileSync(postFilePath).toString();
|
||||
} catch (error) {
|
||||
console.log(`No README.mdx provided for: ${appDirname}`);
|
||||
console.log(`No DESCRIPTION.md provided for: ${appDirname}`);
|
||||
source = singleApp.description;
|
||||
}
|
||||
|
||||
const { content, data } = matter(source);
|
||||
const mdxSource = await serialize(content, { scope: data });
|
||||
|
||||
return {
|
||||
props: {
|
||||
source: mdxSource,
|
||||
source: { content, data },
|
||||
data: singleApp,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -20,10 +20,10 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
backPath="/apps"
|
||||
heading={
|
||||
<>
|
||||
<Link href="/apps">
|
||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
||||
</a>
|
||||
<Link
|
||||
href="/apps"
|
||||
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-4 w-24" />}{" "}
|
||||
</Link>
|
||||
{category && (
|
||||
<span className="gap-1 text-gray-600">
|
||||
@@ -47,13 +47,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
}
|
||||
|
||||
export const getStaticPaths = async () => {
|
||||
const appStore = await getAppRegistry();
|
||||
const paths = appStore.reduce((categories, app) => {
|
||||
if (!categories.includes(app.category)) {
|
||||
categories.push(app.category);
|
||||
}
|
||||
return categories;
|
||||
}, [] as string[]);
|
||||
const paths = Object.keys(AppCategories);
|
||||
|
||||
return {
|
||||
paths: paths.map((category) => ({ params: { category } })),
|
||||
|
||||
@@ -11,28 +11,28 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
return (
|
||||
<Shell isPublic large>
|
||||
<div className="text-md flex items-center gap-1 px-4 pb-3 pt-3 font-normal md:px-8 lg:px-0 lg:pt-0">
|
||||
<Link href="/apps">
|
||||
<a className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||
</a>
|
||||
<Link
|
||||
href="/apps"
|
||||
className="inline-flex items-center justify-start gap-1 rounded-sm py-2 text-gray-900">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="mb-16">
|
||||
<div className="grid h-auto w-full grid-cols-5 gap-3">
|
||||
{categories.map((category) => (
|
||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
||||
<a
|
||||
data-testid={`app-store-category-${category.name}`}
|
||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||
<div className="self-center">
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("number_apps", { count: category.count })}{" "}
|
||||
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</a>
|
||||
<Link
|
||||
key={category.name}
|
||||
href={"/apps/categories/" + category.name}
|
||||
data-testid={`app-store-category-${category.name}`}
|
||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
||||
<div className="self-center">
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-sm text-gray-500">
|
||||
{t("number_apps", { count: category.count })}{" "}
|
||||
<Icon.FiArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
))}
|
||||
</div>
|
||||
@@ -44,7 +44,9 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
export const getStaticProps = async () => {
|
||||
const appStore = await getAppRegistry();
|
||||
const categories = appStore.reduce((c, app) => {
|
||||
c[app.category] = c[app.category] ? c[app.category] + 1 : 1;
|
||||
for (const category of app.categories) {
|
||||
c[category] = c[category] ? c[category] + 1 : 1;
|
||||
}
|
||||
return c;
|
||||
}, {} as Record<string, number>);
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { GetServerSideProps, InferGetServerSidePropsType } from "next";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { ChangeEventHandler, useState } from "react";
|
||||
|
||||
import { getAppRegistry, getAppRegistryWithCredentials } from "@calcom/app-store/_appRegistry";
|
||||
@@ -6,7 +6,10 @@ import { classNames } from "@calcom/lib";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { AppCategories } from "@calcom/prisma/client";
|
||||
import { AllApps, AppsLayout, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { AllApps, AppStoreCategories, Icon, TextField, TrendingAppsSlider } from "@calcom/ui";
|
||||
|
||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||
|
||||
import { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
@@ -30,10 +33,7 @@ function AppsSearch({
|
||||
);
|
||||
}
|
||||
|
||||
export default function Apps({
|
||||
categories,
|
||||
appStore,
|
||||
}: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
||||
export default function Apps({ categories, appStore }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const [searchText, setSearchText] = useState<string | undefined>(undefined);
|
||||
|
||||
@@ -52,12 +52,16 @@ export default function Apps({
|
||||
<TrendingAppsSlider items={appStore} />
|
||||
</>
|
||||
)}
|
||||
<AllApps apps={appStore} searchText={searchText} />
|
||||
<AllApps
|
||||
apps={appStore}
|
||||
searchText={searchText}
|
||||
categories={categories.map((category) => category.name)}
|
||||
/>
|
||||
</AppsLayout>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps: GetServerSideProps = async (context) => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssg = await ssgInit(context);
|
||||
|
||||
const session = await getSession(context);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -183,6 +183,7 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
||||
buttonRaw={
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid={`connect-${variant || "other"}-apps`}
|
||||
href={variant ? `/apps/categories/${variant}` : "/apps/categories/other"}>
|
||||
{t(`connect_${variant || "other"}_apps`)}
|
||||
</Button>
|
||||
|
||||
@@ -40,7 +40,7 @@ export default function Error() {
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 sm:mt-6">
|
||||
<Link href="/auth/login" passHref>
|
||||
<Link href="/auth/login" passHref legacyBehavior>
|
||||
<Button className="flex w-full justify-center">{t("go_back_login")}</Button>
|
||||
</Link>
|
||||
</div>
|
||||
|
||||
@@ -81,7 +81,7 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
||||
<h2 className="text-center text-3xl font-extrabold text-gray-900">{t("request_is_expired")}</h2>
|
||||
</div>
|
||||
<p>{t("request_is_expired_instructions")}</p>
|
||||
<Link href="/auth/forgot-password" passHref>
|
||||
<Link href="/auth/forgot-password" passHref legacyBehavior>
|
||||
<button
|
||||
type="button"
|
||||
className="flex w-full justify-center px-4 py-2 text-sm font-medium text-blue-600 focus:outline-none focus:ring-2 focus:ring-black focus:ring-offset-2">
|
||||
|
||||
@@ -94,8 +94,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
footerText={
|
||||
!success && (
|
||||
<>
|
||||
<Link href="/auth/login">
|
||||
<a className="font-medium text-neutral-900">{t("back_to_signin")}</a>
|
||||
<Link href="/auth/login" className="font-medium text-neutral-900">
|
||||
{t("back_to_signin")}
|
||||
</Link>
|
||||
</>
|
||||
)
|
||||
|
||||
@@ -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,11 +132,12 @@ export default function Login({
|
||||
{...register("email")}
|
||||
/>
|
||||
<div className="relative">
|
||||
<div className="absolute right-0 -top-[6px] z-10">
|
||||
<Link href="/auth/forgot-password">
|
||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
||||
{t("forgot")}
|
||||
</a>
|
||||
<div className="absolute -top-[6px] z-10 ltr:right-0 rtl:left-0">
|
||||
<Link
|
||||
href="/auth/forgot-password"
|
||||
tabIndex={-1}
|
||||
className="text-sm font-medium text-gray-600">
|
||||
{t("forgot")}
|
||||
</Link>
|
||||
</div>
|
||||
<PasswordField
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { UserPermissionRole } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
@@ -24,7 +27,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
{
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description"),
|
||||
content: <AdminAppsList baseURL="/auth/setup" />,
|
||||
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
|
||||
isLoading: false,
|
||||
},
|
||||
];
|
||||
@@ -32,14 +35,33 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
return (
|
||||
<>
|
||||
<main className="flex items-center bg-gray-100 print:h-full">
|
||||
<WizardForm href="/auth/setup" steps={steps} disableNavigation={shouldDisable} />
|
||||
<WizardForm
|
||||
href="/auth/setup"
|
||||
steps={steps}
|
||||
nextLabel={t("next_step_text")}
|
||||
finishLabel={t("finish")}
|
||||
prevLabel={t("prev_step")}
|
||||
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getServerSideProps = async () => {
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
|
||||
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/404`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
userCount,
|
||||
|
||||
@@ -97,7 +97,7 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
<TextField
|
||||
addOnLeading={
|
||||
!longWebsiteUrl && (
|
||||
<span className="items-centerpx-3 inline-flex rounded-none text-sm text-gray-500">
|
||||
<span className="inline-flex items-center rounded-none px-3 text-sm text-gray-500">
|
||||
{process.env.NEXT_PUBLIC_WEBSITE_URL}/
|
||||
</span>
|
||||
)
|
||||
|
||||
@@ -1,18 +1,30 @@
|
||||
import { useRouter } from "next/router";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
|
||||
const StepDone = () => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||
<form
|
||||
id="wizard-step-1"
|
||||
name="wizard-step-1"
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
}}>
|
||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
<div className="max-w-[420px] text-center">
|
||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -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">
|
||||
|
||||
@@ -174,74 +174,70 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<div className="flex items-baseline sm:mt-0">
|
||||
{/* TODO: Find a better way to guarantee alignment, but for now this'll do. */}
|
||||
<Icon.FiArrowLeft className=" mr-3 text-transparent hover:cursor-pointer" />
|
||||
<div className="w-full">
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||
updateMutation.mutate({
|
||||
scheduleId: schedule,
|
||||
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||
...values,
|
||||
});
|
||||
}}
|
||||
className="-mx-4 flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
||||
<div className=" py-5 pr-4 sm:p-6">
|
||||
<h3 className="mb-5 text-base font-medium leading-6 text-gray-900">
|
||||
{t("change_start_end")}
|
||||
</h3>
|
||||
{typeof me.data?.weekStart === "string" && (
|
||||
<Schedule
|
||||
control={control}
|
||||
name="schedule"
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
me.data?.weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||
<div className="w-full">
|
||||
<Form
|
||||
form={form}
|
||||
id="availability-form"
|
||||
handleSubmit={async ({ dateOverrides, ...values }) => {
|
||||
updateMutation.mutate({
|
||||
scheduleId: schedule,
|
||||
dateOverrides: dateOverrides.flatMap((override) => override.ranges),
|
||||
...values,
|
||||
});
|
||||
}}
|
||||
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1 divide-y divide-neutral-200 rounded-md border">
|
||||
<div className=" py-5 sm:p-6">
|
||||
<h3 className="mb-2 px-5 text-base font-medium leading-6 text-gray-900 sm:pl-0">
|
||||
{t("change_start_end")}
|
||||
</h3>
|
||||
{typeof me.data?.weekStart === "string" && (
|
||||
<Schedule
|
||||
control={control}
|
||||
name="schedule"
|
||||
weekStart={
|
||||
["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"].indexOf(
|
||||
me.data?.weekStart
|
||||
) as 0 | 1 | 2 | 3 | 4 | 5 | 6
|
||||
}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="rounded-md">
|
||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||
<div className="mt-3 flex">
|
||||
<Button href="/availability/troubleshoot" color="secondary">
|
||||
{t("launch_troubleshooter")}
|
||||
</Button>
|
||||
</div>
|
||||
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||
</div>
|
||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
</label>
|
||||
<Controller
|
||||
name="timeZone"
|
||||
render={({ field: { onChange, value } }) =>
|
||||
value ? (
|
||||
<TimezoneSelect
|
||||
value={value}
|
||||
className="focus:border-brand mt-1 block w-72 rounded-md border-gray-300 text-sm"
|
||||
onChange={(timezone) => onChange(timezone.value)}
|
||||
/>
|
||||
) : (
|
||||
<SelectSkeletonLoader className="w-72" />
|
||||
)
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="rounded-md">
|
||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||
<div className="mt-3 flex">
|
||||
<Button href="/availability/troubleshoot" color="secondary">
|
||||
{t("launch_troubleshooter")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</div>
|
||||
</Form>
|
||||
</div>
|
||||
</Shell>
|
||||
);
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -168,17 +167,18 @@ const querySchema = z.object({
|
||||
changes: stringToBoolean,
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
formerTime: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Success(props: SuccessProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const {
|
||||
allRemainingBookings,
|
||||
isSuccessBookingPage,
|
||||
cancel: isCancellationMode,
|
||||
changes,
|
||||
formerTime,
|
||||
} = querySchema.parse(router.query);
|
||||
|
||||
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||
@@ -353,10 +353,10 @@ export default function Success(props: SuccessProps) {
|
||||
)}
|
||||
{userIsOwner && !isEmbed && (
|
||||
<div className="mt-2 ml-4 -mb-4">
|
||||
<Link href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}>
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
|
||||
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
</a>
|
||||
<Link
|
||||
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
|
||||
className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
|
||||
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -425,9 +425,11 @@ export default function Success(props: SuccessProps) {
|
||||
<p className="text-neutral-600 dark:text-gray-300">{getTitle()}</p>
|
||||
</div>
|
||||
<div className="border-bookinglightest text-bookingdark dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t pt-8 text-left dark:text-gray-300">
|
||||
{isCancelled && cancellationReason && (
|
||||
{(isCancelled || reschedule) && cancellationReason && (
|
||||
<>
|
||||
<div className="font-medium">{t("reason")}</div>
|
||||
<div className="font-medium">
|
||||
{isCancelled ? t("reason") : t("reschedule_reason_success_page")}
|
||||
</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">{cancellationReason}</div>
|
||||
</>
|
||||
)}
|
||||
@@ -435,6 +437,19 @@ export default function Success(props: SuccessProps) {
|
||||
<div className="col-span-2 mb-6 last:mb-0">{eventName}</div>
|
||||
<div className="font-medium">{t("when")}</div>
|
||||
<div className="col-span-2 mb-6 last:mb-0">
|
||||
{reschedule && !!formerTime && (
|
||||
<p className="line-through">
|
||||
<RecurringBookings
|
||||
eventType={props.eventType}
|
||||
duration={calculatedDuration}
|
||||
recurringBookings={props.recurringBookings}
|
||||
allRemainingBookings={allRemainingBookings}
|
||||
date={dayjs(formerTime)}
|
||||
is24h={is24h}
|
||||
isCancelled={isCancelled}
|
||||
/>
|
||||
</p>
|
||||
)}
|
||||
<RecurringBookings
|
||||
eventType={props.eventType}
|
||||
duration={calculatedDuration}
|
||||
@@ -558,7 +573,9 @@ export default function Success(props: SuccessProps) {
|
||||
{!props.recurringBookings && (
|
||||
<span className="text-bookinglight inline text-gray-700">
|
||||
<span className="underline">
|
||||
<Link href={`/reschedule/${bookingInfo?.uid}`}>{t("reschedule")}</Link>
|
||||
<Link href={`/reschedule/${bookingInfo?.uid}`} legacyBehavior>
|
||||
{t("reschedule")}
|
||||
</Link>
|
||||
</span>
|
||||
<span className="mx-2">{t("or_lowercase")}</span>
|
||||
</span>
|
||||
@@ -618,17 +635,16 @@ export default function Success(props: SuccessProps) {
|
||||
? "&recur=" +
|
||||
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">
|
||||
<svg
|
||||
className="-mt-1.5 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Google</title>
|
||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||
</svg>
|
||||
</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"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Google</title>
|
||||
<path d="M12.48 10.92v3.28h7.84c-.24 1.84-.853 3.187-1.787 4.133-1.147 1.147-2.933 2.4-6.053 2.4-4.827 0-8.6-3.893-8.6-8.72s3.773-8.72 8.6-8.72c2.6 0 4.507 1.027 5.907 2.347l2.307-2.307C18.747 1.44 16.133 0 12.48 0 5.867 0 .307 5.387.307 12s5.56 12 12.173 12c3.573 0 6.267-1.173 8.373-3.36 2.16-2.16 2.84-5.213 2.84-7.667 0-.76-.053-1.467-.173-2.053H12.48z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
@@ -642,19 +658,17 @@ export default function Success(props: SuccessProps) {
|
||||
"&subject=" +
|
||||
eventName
|
||||
) + (location ? "&location=" + location : "")
|
||||
}>
|
||||
<a
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Microsoft Outlook</title>
|
||||
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Microsoft Outlook</title>
|
||||
<path d="M7.88 12.04q0 .45-.11.87-.1.41-.33.74-.22.33-.58.52-.37.2-.87.2t-.85-.2q-.35-.21-.57-.55-.22-.33-.33-.75-.1-.42-.1-.86t.1-.87q.1-.43.34-.76.22-.34.59-.54.36-.2.87-.2t.86.2q.35.21.57.55.22.34.31.77.1.43.1.88zM24 12v9.38q0 .46-.33.8-.33.32-.8.32H7.13q-.46 0-.8-.33-.32-.33-.32-.8V18H1q-.41 0-.7-.3-.3-.29-.3-.7V7q0-.41.3-.7Q.58 6 1 6h6.5V2.55q0-.44.3-.75.3-.3.75-.3h12.9q.44 0 .75.3.3.3.3.75V10.85l1.24.72h.01q.1.07.18.18.07.12.07.25zm-6-8.25v3h3v-3zm0 4.5v3h3v-3zm0 4.5v1.83l3.05-1.83zm-5.25-9v3h3.75v-3zm0 4.5v3h3.75v-3zm0 4.5v2.03l2.41 1.5 1.34-.8v-2.73zM9 3.75V6h2l.13.01.12.04v-2.3zM5.98 15.98q.9 0 1.6-.3.7-.32 1.19-.86.48-.55.73-1.28.25-.74.25-1.61 0-.83-.25-1.55-.24-.71-.71-1.24t-1.15-.83q-.68-.3-1.55-.3-.92 0-1.64.3-.71.3-1.2.85-.5.54-.75 1.3-.25.74-.25 1.63 0 .85.26 1.56.26.72.74 1.23.48.52 1.17.81.69.3 1.56.3zM7.5 21h12.39L12 16.08V17q0 .41-.3.7-.29.3-.7.3H7.5zm15-.13v-7.24l-5.9 3.54Z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link
|
||||
href={
|
||||
@@ -668,34 +682,31 @@ export default function Success(props: SuccessProps) {
|
||||
"&subject=" +
|
||||
eventName
|
||||
) + (location ? "&location=" + location : "")
|
||||
}>
|
||||
<a
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Microsoft Office</title>
|
||||
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
||||
</svg>
|
||||
</a>
|
||||
}
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
target="_blank">
|
||||
<svg
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24">
|
||||
<title>Microsoft Office</title>
|
||||
<path d="M21.53 4.306v15.363q0 .807-.472 1.433-.472.627-1.253.85l-6.888 1.974q-.136.037-.29.055-.156.019-.293.019-.396 0-.72-.105-.321-.106-.656-.292l-4.505-2.544q-.248-.137-.391-.366-.143-.23-.143-.515 0-.434.304-.738.304-.305.739-.305h5.831V4.964l-4.38 1.563q-.533.187-.856.658-.322.472-.322 1.03v8.078q0 .496-.248.912-.25.416-.683.651l-2.072 1.13q-.286.148-.571.148-.497 0-.844-.347-.348-.347-.348-.844V6.563q0-.62.33-1.19.328-.571.874-.881L11.07.285q.248-.136.534-.21.285-.075.57-.075.211 0 .38.031.166.031.364.093l6.888 1.899q.384.11.7.329.317.217.547.52.23.305.353.67.125.367.125.764zm-1.588 15.363V4.306q0-.273-.16-.478-.163-.204-.423-.28l-3.388-.93q-.397-.111-.794-.23-.397-.117-.794-.216v19.68l4.976-1.427q.26-.074.422-.28.161-.204.161-.477z" />
|
||||
</svg>
|
||||
</Link>
|
||||
<Link href={"data:text/calendar," + eventLink()}>
|
||||
<a
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
download={props.eventType.title + ".ics"}>
|
||||
<svg
|
||||
version="1.1"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 1000"
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
||||
<title>{t("other")}</title>
|
||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||
</svg>
|
||||
</a>
|
||||
<Link
|
||||
href={"data:text/calendar," + eventLink()}
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-neutral-200 px-3 py-2 dark:border-neutral-700 dark:text-white"
|
||||
download={props.eventType.title + ".ics"}>
|
||||
<svg
|
||||
version="1.1"
|
||||
fill="currentColor"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 1000 1000"
|
||||
className="mr-1 -mt-1.5 inline-block h-4 w-4">
|
||||
<title>{t("other")}</title>
|
||||
<path d="M971.3,154.9c0-34.7-28.2-62.9-62.9-62.9H611.7c-1.3,0-2.6,0.1-3.9,0.2V10L28.7,87.3v823.4L607.8,990v-84.6c1.3,0.1,2.6,0.2,3.9,0.2h296.7c34.7,0,62.9-28.2,62.9-62.9V154.9z M607.8,636.1h44.6v-50.6h-44.6v-21.9h44.6v-50.6h-44.6v-92h277.9v230.2c0,3.8-3.1,7-7,7H607.8V636.1z M117.9,644.7l-50.6-2.4V397.5l50.6-2.2V644.7z M288.6,607.3c17.6,0.6,37.3-2.8,49.1-7.2l9.1,48c-11,5.1-35.6,9.9-66.9,8.3c-85.4-4.3-127.5-60.7-127.5-132.6c0-86.2,57.8-136.7,133.2-140.1c30.3-1.3,53.7,4,64.3,9.2l-12.2,48.9c-12.1-4.9-28.8-9.2-49.5-8.6c-45.3,1.2-79.5,30.1-79.5,87.4C208.8,572.2,237.8,605.7,288.6,607.3z M455.5,665.2c-32.4-1.6-63.7-11.3-79.1-20.5l12.6-50.7c16.8,9.1,42.9,18.5,70.4,19.4c30.1,1,46.3-10.7,46.3-29.3c0-17.8-14-28.1-48.8-40.6c-46.9-16.4-76.8-41.7-76.8-81.5c0-46.6,39.3-84.1,106.8-87.1c33.3-1.5,58.3,4.2,76.5,11.2l-15.4,53.3c-12.1-5.3-33.5-12.8-62.3-12c-28.3,0.8-41.9,13.6-41.9,28.1c0,17.8,16.1,25.5,53.6,39c52.9,18.5,78.4,45.3,78.4,86.4C575.6,629.7,536.2,669.2,455.5,665.2z M935.3,842.7c0,14.9-12.1,27-27,27H611.7c-1.3,0-2.6-0.2-3.9-0.4V686.2h270.9c19.2,0,34.9-15.6,34.9-34.9V398.4c0-19.2-15.6-34.9-34.9-34.9h-47.1v-32.3H808v32.3h-44.8v-32.3h-22.7v32.3h-43.3v-32.3h-22.7v32.3H628v-32.3h-20.2v-203c1.31.2,2.6-0.4,3.9-0.4h296.7c14.9,0,27,12.1,27,27L935.3,842.7L935.3,842.7z" />
|
||||
</svg>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
@@ -790,10 +801,10 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(0, 4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
||||
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||
<br />
|
||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||
<span className="text-bookinglight">({timeZone()})</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -808,10 +819,10 @@ export function RecurringBookings({
|
||||
{eventType.recurringEvent?.count &&
|
||||
recurringBookingsSorted.slice(4).map((dateStr: string, idx: number) => (
|
||||
<div key={idx} className={classNames("mb-2", isCancelled ? "line-through" : "")}>
|
||||
{dayjs.tz(dateStr, timeZone()).format("MMMM DD, YYYY")}
|
||||
{dayjs.tz(dateStr, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||
<br />
|
||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone())} -{" "}
|
||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||
{formatTime(dateStr, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||
{formatTime(dayjs(dateStr).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||
<span className="text-bookinglight">({timeZone()})</span>
|
||||
</div>
|
||||
))}
|
||||
@@ -824,10 +835,10 @@ export function RecurringBookings({
|
||||
|
||||
return (
|
||||
<div className={classNames(isCancelled ? "line-through" : "")}>
|
||||
{dayjs.tz(date, timeZone()).format("MMMM DD, YYYY")}
|
||||
{dayjs.tz(date, timeZone()).format("dddd, DD MMMM YYYY")}
|
||||
<br />
|
||||
{formatTime(date, is24h ? 24 : 12, timeZone())} -{" "}
|
||||
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone())}{" "}
|
||||
{formatTime(date, is24h ? 24 : 12, timeZone().toLowerCase())} -{" "}
|
||||
{formatTime(dayjs(date).add(duration, "m"), is24h ? 24 : 12, timeZone().toLowerCase())}{" "}
|
||||
<span className="text-bookinglight">({timeZone()})</span>
|
||||
</div>
|
||||
);
|
||||
@@ -1034,6 +1045,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,
|
||||
|
||||
@@ -1,450 +0,0 @@
|
||||
import { BookingStatus } from "@prisma/client";
|
||||
import { createHmac } from "crypto";
|
||||
import { instance } from "gaxios";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getRecurringWhen } from "@calcom/emails/src/components/WhenInfo";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||
import { processBookingConfirmation } from "@calcom/lib/server/queries/bookings/confirm";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { TRPCError } from "@calcom/trpc/server";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
|
||||
import { HeadSeo } from "@components/seo/head-seo";
|
||||
|
||||
enum DirectAction {
|
||||
"accept" = "accept",
|
||||
"reject" = "reject",
|
||||
}
|
||||
const actionSchema = z.nativeEnum(DirectAction);
|
||||
|
||||
const refineParse = (result: z.SafeParseReturnType<any, any>, context: z.RefinementCtx) => {
|
||||
if (result.success === false) {
|
||||
result.error.issues.map((issue) => context.addIssue(issue));
|
||||
}
|
||||
};
|
||||
|
||||
const CALENDSO_ENCRYPTION_KEY = process.env.CALENDSO_ENCRYPTION_KEY || "";
|
||||
|
||||
const pageErrors = {
|
||||
signature_mismatch: "Direct link signature doesn't match signed data",
|
||||
booking_not_found: "Direct link booking not found",
|
||||
user_not_found: "Direct link booking user not found",
|
||||
};
|
||||
|
||||
const requestSchema = z.object({
|
||||
link: z
|
||||
.array(z.string())
|
||||
.max(4)
|
||||
.superRefine((data, ctx) => {
|
||||
refineParse(actionSchema.safeParse(data[0]), ctx);
|
||||
const signedData = `${data[1]}/${data[2]}`;
|
||||
const sig = createHmac("sha1", CALENDSO_ENCRYPTION_KEY).update(signedData).digest("base64");
|
||||
if (data[3] !== sig) {
|
||||
ctx.addIssue({
|
||||
message: pageErrors.signature_mismatch,
|
||||
code: "custom",
|
||||
});
|
||||
console.log(signedData, data, data[3], "==", sig);
|
||||
}
|
||||
}),
|
||||
reason: z.string().optional(),
|
||||
});
|
||||
|
||||
function bookingContent(status: BookingStatus | undefined | null) {
|
||||
switch (status) {
|
||||
case BookingStatus.PENDING:
|
||||
// Trying to reject booking without reason
|
||||
return {
|
||||
iconColor: "gray",
|
||||
Icon: Icon.FiCalendar,
|
||||
titleKey: "event_awaiting_approval",
|
||||
subtitleKey: "someone_requested_an_event",
|
||||
};
|
||||
case BookingStatus.ACCEPTED:
|
||||
// Booking was acepted successfully
|
||||
return {
|
||||
iconColor: "green",
|
||||
Icon: Icon.FiCheck,
|
||||
titleKey: "booking_confirmed",
|
||||
subtitleKey: "emailed_you_and_any_other_attendees",
|
||||
};
|
||||
case BookingStatus.REJECTED:
|
||||
// Booking was rejected successfully
|
||||
return {
|
||||
iconColor: "red",
|
||||
Icon: Icon.FiX,
|
||||
titleKey: "booking_rejection_success",
|
||||
subtitleKey: "emailed_you_and_any_other_attendees",
|
||||
};
|
||||
default:
|
||||
// Booking was already accepted or rejected
|
||||
return {
|
||||
iconColor: "yellow",
|
||||
Icon: Icon.FiAlertTriangle,
|
||||
titleKey: "booking_already_accepted_rejected",
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default function Directlink({ booking, reason, status }: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const acceptPath = router.asPath.replace("reject", "accept");
|
||||
const rejectPath = router.asPath.replace("accept", "reject");
|
||||
const [cancellationReason, setCancellationReason] = useState("");
|
||||
function getRecipientStart(format: string) {
|
||||
return dayjs(booking.startTime).tz(booking?.user?.timeZone).format(format);
|
||||
}
|
||||
|
||||
function getRecipientEnd(format: string) {
|
||||
return dayjs(booking.endTime).tz(booking?.user?.timeZone).format(format);
|
||||
}
|
||||
const organizer = {
|
||||
...booking.attendees[0],
|
||||
language: {
|
||||
translate: t,
|
||||
locale: booking.attendees[0].locale ?? "en",
|
||||
},
|
||||
};
|
||||
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(booking.location)
|
||||
? booking.location[0]
|
||||
: // If there is no location set then we default to Cal Video
|
||||
"integrations:daily";
|
||||
const locationToDisplay = getSuccessPageLocationMessage(location, t);
|
||||
const content = bookingContent(status);
|
||||
const recurringInfo = getRecurringWhen({
|
||||
recurringEvent: booking.eventType?.recurringEvent,
|
||||
attendee: organizer,
|
||||
});
|
||||
return (
|
||||
<>
|
||||
<HeadSeo
|
||||
title={t(content.titleKey)}
|
||||
description=""
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
<div className="dark:bg-darkgray-50 desktop-transparent min-h-screen bg-gray-100 px-4">
|
||||
<main className="mx-auto max-w-3xl">
|
||||
<div className="z-50 overflow-y-auto ">
|
||||
<div className="flex items-end justify-center px-4 pt-4 pb-20 text-center sm:block sm:p-0">
|
||||
<div className="inset-0 my-4 transition-opacity sm:my-0" aria-hidden="true">
|
||||
<div
|
||||
className="main dark:bg-darkgray-100 inline-block transform overflow-hidden rounded-lg border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 sm:my-[68px] sm:w-full sm:max-w-xl sm:py-8 sm:align-middle"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="modal-headline">
|
||||
<div
|
||||
className={`flex h-12 w-12 items-center justify-center rounded-full sm:mx-auto bg-${content.iconColor}-100`}>
|
||||
<content.Icon className={`h-5 w-5 text-${content.iconColor}-600`} />
|
||||
</div>
|
||||
<div className="mt-6 mb-8 last:mb-0 sm:text-center">
|
||||
<h3
|
||||
className="text-2xl font-semibold leading-6 text-neutral-900 dark:text-white"
|
||||
id="modal-headline">
|
||||
{t(content.titleKey)}
|
||||
</h3>
|
||||
{content.subtitleKey && (
|
||||
<div className="mt-3">
|
||||
<p className="text-neutral-600 dark:text-gray-300">{t(content.subtitleKey)}</p>
|
||||
</div>
|
||||
)}
|
||||
<div className="dark:border-darkgray-300 mt-8 grid grid-cols-3 border-t border-[#e1e1e1] pt-8 text-left text-[#313131] dark:text-gray-300">
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("what")}</div>
|
||||
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">{booking.title}</div>
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("when")}</div>
|
||||
<div className="col-span-3 mb-6 last:mb-0 sm:col-span-2">
|
||||
{recurringInfo !== "" && (
|
||||
<>
|
||||
{recurringInfo}
|
||||
<br />
|
||||
</>
|
||||
)}
|
||||
{booking.eventType.recurringEvent?.count ? `${t("starting")} ` : ""}
|
||||
{t(getRecipientStart("dddd").toLowerCase())},{" "}
|
||||
{t(getRecipientStart("MMMM").toLowerCase())} {getRecipientStart("D, YYYY")}
|
||||
<br />
|
||||
{getRecipientStart("h:mma")} - {getRecipientEnd("h:mma")}{" "}
|
||||
<span style={{ color: "#888888" }}>({booking.attendees[0].timeZone})</span>
|
||||
</div>
|
||||
{(booking?.user || booking?.attendees) && (
|
||||
<>
|
||||
<div className="col-span-3 font-medium sm:col-span-1">{t("who")}</div>
|
||||
<div className="col-span-3 last:mb-0 sm:col-span-2">
|
||||
<>
|
||||
{booking?.user && (
|
||||
<div className="mb-3">
|
||||
<p>{booking.user.name}</p>
|
||||
<p className="text-[#888888]">{booking.user.email}</p>
|
||||
</div>
|
||||
)}
|
||||
{booking?.attendees.map((attendee) => (
|
||||
<div key={attendee.name} className="mb-3 last:mb-0">
|
||||
{attendee.name && <p>{attendee.name}</p>}
|
||||
<p className="text-[#888888]">{attendee.email}</p>
|
||||
</div>
|
||||
))}
|
||||
</>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{locationToDisplay && (
|
||||
<>
|
||||
<div className="col-span-3 mt-6 font-medium sm:col-span-1">{t("where")}</div>
|
||||
<div className="col-span-3 mt-6 sm:col-span-2">
|
||||
{locationToDisplay.startsWith("http") ? (
|
||||
<a title="Meeting Link" href={locationToDisplay}>
|
||||
{locationToDisplay}
|
||||
</a>
|
||||
) : (
|
||||
locationToDisplay
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{booking?.description && (
|
||||
<>
|
||||
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
|
||||
{t("additional_notes")}
|
||||
</div>
|
||||
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
|
||||
<p>{booking.description}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
{status === BookingStatus.REJECTED && reason && (
|
||||
<>
|
||||
<div className="col-span-3 mt-9 font-medium sm:col-span-1">
|
||||
{t("rejection_reason")}
|
||||
</div>
|
||||
<div className="col-span-3 mb-2 mt-9 sm:col-span-2">
|
||||
<p>{reason}</p>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
{status === BookingStatus.PENDING && reason === undefined && (
|
||||
<>
|
||||
<hr className="mt-6" />
|
||||
<div className="mt-5 text-left sm:mt-6">
|
||||
<label className="font-medium text-[#313131] dark:text-white">
|
||||
{`${t("rejection_reason")} (${t("optional").toLowerCase()})`}
|
||||
</label>
|
||||
<TextArea
|
||||
value={cancellationReason}
|
||||
onChange={(e) => setCancellationReason(e.target.value)}
|
||||
className="mt-2 mb-4 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white "
|
||||
rows={3}
|
||||
/>
|
||||
<div className="flex flex-col-reverse rtl:space-x-reverse">
|
||||
<div className="ml-auto flex w-full justify-end space-x-4">
|
||||
<Button
|
||||
color="secondary"
|
||||
className="hidden text-center sm:block"
|
||||
href={acceptPath}>
|
||||
{t("booking_accept_intent")}
|
||||
</Button>
|
||||
<Button
|
||||
className="hidden sm:block"
|
||||
onClick={async () => {
|
||||
router.push(
|
||||
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
|
||||
);
|
||||
}}>
|
||||
{t("rejection_confirmation")}
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
className="block text-center sm:hidden"
|
||||
href={acceptPath}>
|
||||
{t("accept")}
|
||||
</Button>
|
||||
<Button
|
||||
className="block sm:hidden"
|
||||
onClick={async () => {
|
||||
router.push(
|
||||
`${rejectPath}?reason=${encodeURIComponent(cancellationReason)}`
|
||||
);
|
||||
}}>
|
||||
{t("reject")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</main>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const parsedQuery = requestSchema.safeParse(context.query);
|
||||
|
||||
// Parsing error, showing error 500 with message
|
||||
if (parsedQuery.success === false) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${parsedQuery.error.errors[0].message.concat(
|
||||
" accessing " + context.resolvedUrl
|
||||
)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const {
|
||||
link: [action, email, bookingUid],
|
||||
reason,
|
||||
} = parsedQuery.data;
|
||||
|
||||
const isAccept = action === DirectAction.accept;
|
||||
|
||||
const bookingRaw = await prisma?.booking.findFirst({
|
||||
where: {
|
||||
uid: bookingUid,
|
||||
user: {
|
||||
email,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
location: true,
|
||||
description: true,
|
||||
id: true,
|
||||
recurringEventId: true,
|
||||
status: true,
|
||||
title: true,
|
||||
startTime: true,
|
||||
endTime: true,
|
||||
eventType: {
|
||||
select: {
|
||||
recurringEvent: true,
|
||||
},
|
||||
},
|
||||
attendees: {
|
||||
select: {
|
||||
locale: true,
|
||||
name: true,
|
||||
email: true,
|
||||
timeZone: true,
|
||||
},
|
||||
},
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
email: true,
|
||||
name: true,
|
||||
timeZone: true,
|
||||
locale: true,
|
||||
destinationCalendar: true,
|
||||
credentials: true,
|
||||
username: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
// Booking not found, showing error 500 with message
|
||||
if (!bookingRaw) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${pageErrors.booking_not_found.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const booking = {
|
||||
...bookingRaw,
|
||||
startTime: bookingRaw.startTime.toString(),
|
||||
endTime: bookingRaw.endTime.toString(),
|
||||
eventType: {
|
||||
...bookingRaw.eventType,
|
||||
recurringEvent: parseRecurringEvent(bookingRaw?.eventType?.recurringEvent),
|
||||
},
|
||||
attendees: bookingRaw?.attendees.map((att) => ({
|
||||
...att,
|
||||
language: {
|
||||
locale: att.locale ?? "en",
|
||||
},
|
||||
})),
|
||||
};
|
||||
|
||||
// Booking user not found, showing error 500 with message
|
||||
if (booking.user === null) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${pageErrors.user_not_found.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Booking already accepted or rejected
|
||||
if (booking.status !== BookingStatus.PENDING) {
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: null,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Trying to reject booking without reason
|
||||
if (!isAccept && reason === undefined) {
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: BookingStatus.PENDING,
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
// Booking good to be accepted or rejected, proceeding to mark it
|
||||
let result: { status: BookingStatus | undefined } = { status: undefined };
|
||||
try {
|
||||
result = await processBookingConfirmation(
|
||||
{
|
||||
bookingId: booking.id,
|
||||
user: booking.user,
|
||||
recurringEventId: booking.recurringEventId,
|
||||
confirmed: action === DirectAction.accept,
|
||||
rejectionReason: reason,
|
||||
},
|
||||
prisma
|
||||
);
|
||||
} catch (e) {
|
||||
if (e instanceof TRPCError) {
|
||||
return {
|
||||
redirect: {
|
||||
destination: `/500?error=${e.message.concat(" accessing " + context.resolvedUrl)}`,
|
||||
permanent: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
booking,
|
||||
status: result.status,
|
||||
reason: context.query.reason ?? null,
|
||||
},
|
||||
};
|
||||
}
|
||||
@@ -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() })}
|
||||
|
||||
@@ -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 { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { trpc, RouterOutputs } from "@calcom/trpc/react";
|
||||
import type { BookingLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Form, showToast } from "@calcom/ui";
|
||||
|
||||
@@ -36,10 +34,10 @@ import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
||||
import { EventTeamTab } from "@components/eventtype/EventTeamTab";
|
||||
import { EventTeamWebhooksTab } from "@components/eventtype/EventTeamWebhooksTab";
|
||||
import { EventTypeSingleLayout } from "@components/eventtype/EventTypeSingleLayout";
|
||||
import EventWorkflowsTab from "@components/eventtype/EventWorkfowsTab";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export type FormValues = {
|
||||
@@ -93,32 +91,40 @@ export type CustomInputParsed = typeof customInputSchema._output;
|
||||
|
||||
const querySchema = z.object({
|
||||
tabName: z
|
||||
.enum(["setup", "availability", "apps", "limits", "recurring", "team", "advanced", "workflows"])
|
||||
.enum([
|
||||
"setup",
|
||||
"availability",
|
||||
"apps",
|
||||
"limits",
|
||||
"recurring",
|
||||
"team",
|
||||
"advanced",
|
||||
"workflows",
|
||||
"webhooks",
|
||||
])
|
||||
.optional()
|
||||
.default("setup"),
|
||||
});
|
||||
|
||||
export type EventTypeSetupInfered = inferSSRProps<typeof getServerSideProps>;
|
||||
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
||||
|
||||
const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const {
|
||||
data: { tabName },
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const { data: eventTypeApps } = trpc.viewer.apps.useQuery({
|
||||
extendsFeature: "EventType",
|
||||
});
|
||||
|
||||
const { eventType: dbEventType, locationOptions, team, teamMembers } = props;
|
||||
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
|
||||
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
|
||||
const [eventType, setEventType] = useState(dbEventType);
|
||||
|
||||
const router = useRouter();
|
||||
const { tabName } = querySchema.parse(router.query);
|
||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership } = props;
|
||||
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async ({ eventType: newEventType }) => {
|
||||
setEventType({ ...eventType, slug: newEventType.slug });
|
||||
onSuccess: async () => {
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
@@ -126,6 +132,9 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
"success"
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
let message = "";
|
||||
if (err instanceof HttpError) {
|
||||
@@ -193,6 +202,16 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
),
|
||||
metadata,
|
||||
},
|
||||
resolver: zodResolver(
|
||||
z
|
||||
.object({
|
||||
// Length if string, is converted to a number or it can be a number
|
||||
// Make it optional because it's not submitted from all tabs of the page
|
||||
length: z.union([z.string().transform((val) => +val), z.number()]).optional(),
|
||||
})
|
||||
// TODO: Add schema for other fields later.
|
||||
.passthrough()
|
||||
),
|
||||
});
|
||||
|
||||
const appsMetadata = formMethods.getValues("metadata")?.apps;
|
||||
@@ -219,14 +238,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
/>
|
||||
),
|
||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
||||
team: (
|
||||
<EventTeamTab
|
||||
eventType={eventType}
|
||||
teamMembers={teamMembers}
|
||||
team={team}
|
||||
currentUserMembership={props.currentUserMembership}
|
||||
/>
|
||||
),
|
||||
team: <EventTeamTab eventType={eventType} teamMembers={teamMembers} team={team} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
@@ -237,6 +249,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
workflows={eventType.workflows.map((workflowOnEventType) => workflowOnEventType.workflow)}
|
||||
/>
|
||||
),
|
||||
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
|
||||
} as const;
|
||||
|
||||
return (
|
||||
@@ -248,8 +261,8 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
team={team}
|
||||
isUpdateMutationLoading={updateMutation.isLoading}
|
||||
formMethods={formMethods}
|
||||
disableBorder={tabName === "apps" || tabName === "workflows"}
|
||||
currentUserMembership={props.currentUserMembership}>
|
||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||
currentUserMembership={currentUserMembership}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
@@ -283,7 +296,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
if (metadata?.multipleDuration.length < 1) {
|
||||
throw new Error(t("event_setup_multiple_duration_error"));
|
||||
} else {
|
||||
if (input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
||||
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||
}
|
||||
}
|
||||
@@ -314,6 +327,18 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypePageWrapper = (props: inferSSRProps<typeof getServerSideProps>) => {
|
||||
const { data, isLoading } = trpc.viewer.eventTypes.get.useQuery(
|
||||
{ id: props.type },
|
||||
{
|
||||
initialData: props.initialData,
|
||||
}
|
||||
);
|
||||
|
||||
if (isLoading || !data) return null;
|
||||
return <EventTypePage {...data} />;
|
||||
};
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, query } = context;
|
||||
const session = await getSession({ req });
|
||||
@@ -335,233 +360,27 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
const userSelect = Prisma.validator<Prisma.UserSelect>()({
|
||||
name: true,
|
||||
username: true,
|
||||
id: true,
|
||||
avatar: true,
|
||||
email: true,
|
||||
locale: true,
|
||||
defaultScheduleId: true,
|
||||
});
|
||||
|
||||
const rawEventType = await prisma.eventType.findFirst({
|
||||
where: {
|
||||
AND: [
|
||||
{
|
||||
OR: [
|
||||
{
|
||||
users: {
|
||||
some: {
|
||||
id: session.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
team: {
|
||||
members: {
|
||||
some: {
|
||||
userId: session.user.id,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
userId: session.user.id,
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
id: typeParam,
|
||||
},
|
||||
],
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
slug: true,
|
||||
description: true,
|
||||
length: true,
|
||||
hidden: true,
|
||||
locations: true,
|
||||
eventName: true,
|
||||
customInputs: true,
|
||||
timeZone: true,
|
||||
periodType: true,
|
||||
metadata: true,
|
||||
periodDays: true,
|
||||
periodStartDate: true,
|
||||
periodEndDate: true,
|
||||
periodCountCalendarDays: true,
|
||||
requiresConfirmation: true,
|
||||
recurringEvent: true,
|
||||
hideCalendarNotes: true,
|
||||
disableGuests: true,
|
||||
minimumBookingNotice: true,
|
||||
beforeEventBuffer: true,
|
||||
afterEventBuffer: true,
|
||||
slotInterval: true,
|
||||
hashedLink: true,
|
||||
bookingLimits: true,
|
||||
successRedirectUrl: true,
|
||||
currency: true,
|
||||
team: {
|
||||
select: {
|
||||
id: true,
|
||||
slug: true,
|
||||
members: {
|
||||
where: {
|
||||
accepted: true,
|
||||
},
|
||||
select: {
|
||||
role: true,
|
||||
user: {
|
||||
select: userSelect,
|
||||
},
|
||||
},
|
||||
},
|
||||
try {
|
||||
const res = await getEventTypeById({ eventTypeId: typeParam, userId: session.user.id, prisma });
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
type: typeParam,
|
||||
trpcState: ssr.dehydrate(),
|
||||
initialData: {
|
||||
eventType: res.eventType,
|
||||
locationOptions: res.locationOptions,
|
||||
team: res.team,
|
||||
teamMembers: res.teamMembers,
|
||||
currentUserMembership: res.currentUserMembership,
|
||||
},
|
||||
},
|
||||
users: {
|
||||
select: userSelect,
|
||||
},
|
||||
schedulingType: true,
|
||||
schedule: {
|
||||
select: {
|
||||
id: true,
|
||||
},
|
||||
},
|
||||
userId: true,
|
||||
price: true,
|
||||
destinationCalendar: true,
|
||||
seatsPerTimeSlot: true,
|
||||
seatsShowAttendees: true,
|
||||
workflows: {
|
||||
include: {
|
||||
workflow: {
|
||||
include: {
|
||||
activeOn: {
|
||||
select: {
|
||||
eventType: {
|
||||
select: {
|
||||
id: true,
|
||||
title: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
steps: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
if (!rawEventType) {
|
||||
};
|
||||
} catch (err) {
|
||||
return {
|
||||
notFound: true,
|
||||
};
|
||||
}
|
||||
|
||||
const credentials = await prisma.credential.findMany({
|
||||
where: {
|
||||
userId: session.user.id,
|
||||
app: {
|
||||
enabled: true,
|
||||
},
|
||||
},
|
||||
select: {
|
||||
id: true,
|
||||
type: true,
|
||||
key: true,
|
||||
userId: true,
|
||||
appId: true,
|
||||
invalid: true,
|
||||
},
|
||||
});
|
||||
|
||||
const { locations, metadata, ...restEventType } = rawEventType;
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
const newMetadata = EventTypeMetaDataSchema.parse(metadata || {})!;
|
||||
const apps = newMetadata.apps || {};
|
||||
const eventTypeWithParsedMetadata = { ...rawEventType, metadata: newMetadata };
|
||||
newMetadata.apps = {
|
||||
...apps,
|
||||
stripe: {
|
||||
...getStripeAppData(eventTypeWithParsedMetadata, true),
|
||||
currency:
|
||||
(
|
||||
credentials.find((integration) => integration.type === "stripe_payment")
|
||||
?.key as unknown as StripeData
|
||||
)?.default_currency || "usd",
|
||||
},
|
||||
giphy: getEventTypeAppData(eventTypeWithParsedMetadata, "giphy", true),
|
||||
rainbow: getEventTypeAppData(eventTypeWithParsedMetadata, "rainbow", true),
|
||||
};
|
||||
|
||||
// TODO: How to extract metadata schema from _EventTypeModel to be able to parse it?
|
||||
// const parsedMetaData = _EventTypeModel.parse(newMetadata);
|
||||
const parsedMetaData = newMetadata;
|
||||
|
||||
const parsedCustomInputs = (rawEventType.customInputs || []).map((input) => customInputSchema.parse(input));
|
||||
|
||||
const eventType = {
|
||||
...restEventType,
|
||||
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
||||
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
|
||||
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
||||
locations: locations as unknown as LocationObject[],
|
||||
metadata: parsedMetaData,
|
||||
customInputs: parsedCustomInputs,
|
||||
};
|
||||
|
||||
// backwards compat
|
||||
if (eventType.users.length === 0 && !eventType.team) {
|
||||
const fallbackUser = await prisma.user.findUnique({
|
||||
where: {
|
||||
id: session.user.id,
|
||||
},
|
||||
select: userSelect,
|
||||
});
|
||||
if (!fallbackUser) throw Error("The event type doesn't have user and no fallback user was found");
|
||||
eventType.users.push(fallbackUser);
|
||||
}
|
||||
const currentUser = eventType.users.find((u) => u.id === session.user.id);
|
||||
const t = await getTranslation(currentUser?.locale ?? "en", "common");
|
||||
const integrations = await getEnabledApps(credentials);
|
||||
const locationOptions = getLocationOptions(integrations, t);
|
||||
|
||||
const eventTypeObject = Object.assign({}, eventType, {
|
||||
periodStartDate: eventType.periodStartDate?.toString() ?? null,
|
||||
periodEndDate: eventType.periodEndDate?.toString() ?? null,
|
||||
});
|
||||
|
||||
const teamMembers = eventTypeObject.team
|
||||
? eventTypeObject.team.members.map((member) => {
|
||||
const user = member.user;
|
||||
user.avatar = `${CAL_URL}/${user.username}/avatar.png`;
|
||||
return user;
|
||||
})
|
||||
: [];
|
||||
|
||||
// Find the current users memebership so we can check role to enable/disable deletion.
|
||||
// Sets to null if no membership is found - this must mean we are in a none team event type
|
||||
const currentUserMembership =
|
||||
eventTypeObject.team?.members.find((el) => el.user.id === session.user.id) ?? null;
|
||||
|
||||
return {
|
||||
props: {
|
||||
session,
|
||||
eventType: eventTypeObject,
|
||||
locationOptions,
|
||||
team: eventTypeObject.team || null,
|
||||
teamMembers,
|
||||
currentUserMembership,
|
||||
trpcState: ssr.dehydrate(),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export default EventTypePage;
|
||||
export default EventTypePageWrapper;
|
||||
|
||||
@@ -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";
|
||||
@@ -64,27 +64,26 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Link href={`/event-types/${type.id}?tabName=setup`}>
|
||||
<a
|
||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
<div>
|
||||
<span
|
||||
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
||||
data-testid={"event-type-title-" + type.id}>
|
||||
{type.title}
|
||||
<Link
|
||||
href={`/event-types/${type.id}?tabName=setup`}
|
||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
<div>
|
||||
<span
|
||||
className="font-semibold text-gray-700 ltr:mr-1 rtl:ml-1"
|
||||
data-testid={"event-type-title-" + type.id}>
|
||||
{type.title}
|
||||
</span>
|
||||
<small
|
||||
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: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")}
|
||||
</span>
|
||||
<small
|
||||
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">
|
||||
{t("readonly") as string}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</a>
|
||||
)}
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
</Link>
|
||||
);
|
||||
};
|
||||
@@ -258,7 +257,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
const calLink = `${CAL_URL}/${embedLink}`;
|
||||
return (
|
||||
<li key={type.id}>
|
||||
<div className="flex w-full items-center justify-between hover:bg-neutral-50">
|
||||
<div className="flex w-full items-center justify-between hover:bg-gray-50">
|
||||
<div className="group flex w-full max-w-full items-center justify-between overflow-hidden px-4 py-4 sm:px-6">
|
||||
{!(firstItem && firstItem.id === type.id) && (
|
||||
<button
|
||||
@@ -297,7 +296,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{t("hidden")}
|
||||
</Badge>
|
||||
)}
|
||||
<Tooltip content={t("show_eventtype_on_profile") as string}>
|
||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||
<div className="self-center rounded-md p-2 hover:bg-gray-200">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
@@ -310,7 +309,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
</Tooltip>
|
||||
|
||||
<ButtonGroup combined>
|
||||
<Tooltip content={t("preview") as string}>
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
@@ -320,7 +319,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t("copy_link") as string}>
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
size="icon"
|
||||
@@ -350,7 +349,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
data-testid={"event-type-edit-" + type.id}
|
||||
StartIcon={Icon.FiEdit2}
|
||||
onClick={() => router.push("/event-types/" + type.id)}>
|
||||
{t("edit") as string}
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
@@ -359,7 +358,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => openDuplicateModal(type)}>
|
||||
{t("duplicate") as string}
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
@@ -383,7 +382,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
}}
|
||||
StartIcon={Icon.FiTrash}
|
||||
className="w-full rounded-none">
|
||||
{t("delete") as string}
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
)}
|
||||
@@ -394,7 +393,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} />
|
||||
@@ -402,15 +401,13 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<Link href={calLink}>
|
||||
<a target="_blank">
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
className="w-full rounded-none">
|
||||
{t("preview") as string}
|
||||
</Button>
|
||||
</a>
|
||||
<Link href={calLink} target="_blank">
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
className="w-full rounded-none">
|
||||
{t("preview")}
|
||||
</Button>
|
||||
</Link>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
@@ -424,7 +421,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
navigator.clipboard.writeText(calLink);
|
||||
showToast(t("link_copied"), "success");
|
||||
}}>
|
||||
{t("copy_link") as string}
|
||||
{t("copy_link")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
{isNativeShare ? (
|
||||
@@ -445,7 +442,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
.then(() => showToast(t("link_shared"), "success"))
|
||||
.catch(() => showToast(t("failed"), "error"));
|
||||
}}>
|
||||
{t("share") as string}
|
||||
{t("share")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
) : null}
|
||||
@@ -456,7 +453,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
color="minimal"
|
||||
className="w-full rounded-none"
|
||||
StartIcon={Icon.FiEdit}>
|
||||
{t("edit") as string}
|
||||
{t("edit")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem className="outline-none">
|
||||
@@ -467,7 +464,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
data-testid={"event-type-duplicate-" + type.id}
|
||||
StartIcon={Icon.FiCopy}
|
||||
onClick={() => openDuplicateModal(type)}>
|
||||
{t("duplicate") as string}
|
||||
{t("duplicate")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator className="h-px bg-gray-200" />
|
||||
@@ -480,7 +477,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
color="destructive"
|
||||
StartIcon={Icon.FiTrash}
|
||||
className="w-full rounded-none">
|
||||
{t("delete") as string}
|
||||
{t("delete")}
|
||||
</Button>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
@@ -502,7 +499,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
e.preventDefault();
|
||||
deleteEventTypeHandler(deleteDialogTypeId);
|
||||
}}>
|
||||
{t("delete_event_type_description") as string}
|
||||
{t("delete_event_type_description")}
|
||||
</ConfirmationDialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -517,36 +514,32 @@ const EventTypeListHeading = ({
|
||||
return (
|
||||
<div className="mb-4 flex">
|
||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
||||
<a>
|
||||
<Avatar
|
||||
alt={profile?.name || ""}
|
||||
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
|
||||
size={8}
|
||||
className="mt-1 inline ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
</a>
|
||||
<Avatar
|
||||
alt={profile?.name || ""}
|
||||
imageSrc={`${WEBAPP_URL}/${profile.slug}/avatar.png` || undefined}
|
||||
size={8}
|
||||
className="mt-1 inline ltr:mr-2 rtl:ml-2"
|
||||
/>
|
||||
</Link>
|
||||
<div>
|
||||
<Link href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}>
|
||||
<a className="font-bold">{profile?.name || ""}</a>
|
||||
<Link
|
||||
href={teamId ? `/settings/teams/${teamId}/profile` : "/settings/my-account/profile"}
|
||||
className="font-bold">
|
||||
{profile?.name || ""}
|
||||
</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">
|
||||
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
||||
{membershipCount}
|
||||
</Badge>
|
||||
</a>
|
||||
<Badge variant="gray">
|
||||
<Icon.FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
||||
{membershipCount}
|
||||
</Badge>
|
||||
</Link>
|
||||
</span>
|
||||
)}
|
||||
{profile?.slug && (
|
||||
<Link href={`${CAL_URL}/${profile.slug}`}>
|
||||
<a className="block text-xs text-neutral-500">{`${CAL_URL?.replace("https://", "")}/${
|
||||
profile.slug
|
||||
}`}</a>
|
||||
<Link href={`${CAL_URL}/${profile.slug}`} className="block text-xs text-neutral-500">
|
||||
{`${CAL_URL?.replace("https://", "")}/${profile.slug}`}
|
||||
</Link>
|
||||
)}
|
||||
</div>
|
||||
@@ -581,15 +574,11 @@ const EventTypesPage = () => {
|
||||
|
||||
return (
|
||||
<div>
|
||||
<Shell
|
||||
heading={t("event_types_page_title") as string}
|
||||
subtitle={t("event_types_page_subtitle") as string}
|
||||
CTA={<CTA />}>
|
||||
<Shell heading={t("event_types_page_title")} subtitle={t("event_types_page_subtitle")} CTA={<CTA />}>
|
||||
<WithQuery
|
||||
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 +597,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")}
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user