Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7d4cc59cd2 | ||
|
|
a94465ff04 | ||
|
|
4d54ac3ca2 | ||
|
|
15da690270 | ||
|
|
2462e8e597 | ||
|
|
2f628a17df | ||
|
|
1ee3783db3 | ||
|
|
b3d8d7e944 | ||
|
|
a4b7f1af65 | ||
|
|
f7445bf8a1 | ||
|
|
9cad7086d7 | ||
|
|
3345ec61ee | ||
|
|
7797421ce9 | ||
|
|
075a274207 | ||
|
|
a46a31040a | ||
|
|
7a28e1d6f4 | ||
|
|
51fd7fdb3e | ||
|
|
d01963303a | ||
|
|
6877d0f693 | ||
|
|
39040fca9a | ||
|
|
6fa4befc9b | ||
|
|
8c9871c6ce | ||
|
+8 |
a9af2fb255 | ||
|
|
8c150264f4 | ||
|
|
cfc3644d51 | ||
|
|
bdb4a7353b | ||
|
|
30c0e6d1d7 | ||
|
|
390967990a | ||
|
|
cc56602474 | ||
|
|
43a044d59e | ||
|
|
5b8ed9c3ef | ||
|
|
c3d6df2dcf | ||
|
|
9d02cb493c | ||
|
|
26ed478e50 | ||
|
|
09e338970d | ||
|
|
f60399ca8b | ||
|
|
5cf7e32852 | ||
|
|
24be8a63f2 | ||
|
|
75509c97fd | ||
|
|
9b26c7aee6 | ||
|
|
f18e80cfcf | ||
|
|
0a4b1ca014 | ||
|
|
9fa6aced33 | ||
|
|
9093909075 | ||
|
|
832fce91e0 | ||
|
|
6da41fce63 | ||
|
|
3b51cc9aef | ||
|
|
50c8959cfa | ||
|
|
c96d258f5e | ||
|
|
6e45b1c383 | ||
|
|
551ebe863b | ||
|
|
fa7b94bfe9 | ||
|
|
56c5d39cd3 | ||
|
|
7574fb6f44 | ||
|
|
901d1f0e01 | ||
|
|
828fcbe1d9 | ||
|
|
34db46869e | ||
|
|
1f1cab2be1 | ||
|
|
6cf53feaf8 | ||
|
|
376b693d91 | ||
|
|
831aa72d23 | ||
|
|
ef78db7856 | ||
|
|
7ee5967075 | ||
|
|
5b165e6ebf | ||
|
|
624f52c305 | ||
|
|
df25ae6a3d | ||
|
|
7f65d7f3c3 | ||
|
|
c42674595c | ||
|
|
17eb45b0b6 | ||
|
|
48d0edeaf4 | ||
|
|
ed4b31767f | ||
|
|
cebb10bfe4 | ||
|
|
7a6e7c2f1d | ||
|
|
0ffed24359 | ||
|
|
e121615d36 | ||
|
|
4f9aa8bd96 | ||
|
|
64ede37ad6 | ||
|
|
36b6c351ec | ||
|
|
9ab5a3149c | ||
|
|
3b8f436bdf | ||
|
|
77d45a6ec2 | ||
|
|
50c74cf286 | ||
|
|
dcbafced8b | ||
|
|
4679c80ea0 | ||
|
|
291316ee01 | ||
|
|
208fc2f0e4 | ||
|
|
99e71054ff | ||
|
|
9d118d4e34 | ||
|
+6 |
d1a877e70d | ||
|
|
238b509be6 | ||
|
|
c7471b404c | ||
|
|
4db4df8ace | ||
|
|
a15b8fb92e | ||
|
|
a4bed48eaf | ||
|
|
1be6f192d4 | ||
|
|
f1c5838aef | ||
|
|
8e994dcc33 | ||
|
|
506bbe3d31 | ||
|
|
419a84c5a2 | ||
|
|
8cf0050d1f | ||
|
|
6c23813192 | ||
|
|
875f922597 | ||
|
|
c8e3f6494a | ||
|
|
333801d2ae | ||
|
|
60f7528cbc | ||
|
|
0faf294367 | ||
|
|
87c178e53b | ||
|
|
7710b1b718 | ||
|
|
d71c6ccb31 | ||
|
|
33a4117fb5 | ||
|
|
a90419c40b | ||
|
|
16adf86c8c | ||
|
|
3b838c475a | ||
|
|
cf33106d8b | ||
|
|
a63f9ddf61 | ||
|
|
1cf83068b2 | ||
|
|
23de92f818 | ||
|
|
c6fc1f756b | ||
|
|
88b7c506f1 | ||
|
|
755aae969c | ||
|
|
2d50d09ce6 | ||
|
|
a22397dbb4 | ||
|
|
d1076982a7 | ||
|
|
d77e4f3c4a | ||
|
|
b1c0ea4c38 | ||
|
|
425e18f53c | ||
|
|
3611807838 | ||
|
|
5b1cd9e5ce | ||
|
|
00bff598ef | ||
|
|
3a8aa26c78 | ||
|
|
3c761f4451 | ||
|
|
8640eb9f52 | ||
|
|
abb5fd36f3 | ||
|
|
865663646a | ||
|
|
b7c92063c2 | ||
|
|
4f293edb6b | ||
|
|
0c88cf75e0 | ||
|
|
0b93647209 | ||
|
|
b931b00714 | ||
|
|
796a5f8d51 | ||
|
|
905a110377 | ||
|
|
6ccc19985f | ||
|
|
8f71474303 | ||
|
|
5f351230ff | ||
|
|
3e005919c1 | ||
|
|
cfa8fd8b67 | ||
|
|
a888be6e04 | ||
|
|
6aa2cba3ce | ||
|
|
12ab615684 | ||
|
|
1d792a2c6c | ||
|
|
9602ac29fb |
+11
-4
@@ -1,20 +1,21 @@
|
||||
# ********** INDEX **********
|
||||
#
|
||||
# - LICENSE
|
||||
# - LICENSE (DEPRECATED)
|
||||
# - DATABASE
|
||||
# - SHARED
|
||||
# - NEXTAUTH
|
||||
# - E-MAIL SETTINGS
|
||||
|
||||
# - LICENSE *************************************************************************************************
|
||||
# https://github.com/calendso/calendso/blob/main/LICENSE
|
||||
# - LICENSE (DEPRECATED) ************************************************************************************
|
||||
# https://github.com/calcom/cal.com/blob/main/LICENSE
|
||||
#
|
||||
# Summary of terms:
|
||||
# - The codebase has to stay open source, whether it was modified or not
|
||||
# - You can not repackage or sell the codebase
|
||||
# - Acquire a commercial license to remove these terms by visiting: cal.com/sales
|
||||
#
|
||||
# To enable enterprise-only features, fill your license key in here.
|
||||
# To enable enterprise-only features, as an admin, go to /auth/setup to select your license and follow
|
||||
# instructions. This environment variable is deprecated although still supported for backward compatibility.
|
||||
# @see https://console.cal.com
|
||||
CALCOM_LICENSE_KEY=
|
||||
# ***********************************************************************************************************
|
||||
@@ -157,3 +158,9 @@ NEXT_PUBLIC_COMPANY_NAME="Cal.com, Inc."
|
||||
# Set this to true in to disable new signups
|
||||
# NEXT_PUBLIC_DISABLE_SIGNUP=true
|
||||
NEXT_PUBLIC_DISABLE_SIGNUP=
|
||||
|
||||
# Content Security Policy
|
||||
CSP_POLICY=
|
||||
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
@@ -13,7 +13,7 @@ concurrency:
|
||||
jobs:
|
||||
e2e:
|
||||
timeout-minutes: 20
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
env:
|
||||
DATABASE_URL: postgresql://postgres:@localhost:5432/calendso
|
||||
|
||||
@@ -11,7 +11,7 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
jobs:
|
||||
lint:
|
||||
runs-on: ubuntu-latest
|
||||
runs-on: buildjet-4vcpu-ubuntu-2204
|
||||
|
||||
steps:
|
||||
- name: Checkout repo
|
||||
|
||||
@@ -86,3 +86,4 @@ apps/storybook/build-storybook.log
|
||||
apps/api
|
||||
apps/website
|
||||
apps/console
|
||||
apps/auth
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
"keywords": ["react", "typescript", "node", "nextjs", "prisma", "postgres", "trpc"],
|
||||
"addons": [
|
||||
{
|
||||
"plan": "heroku-postgresql:hobby-dev"
|
||||
"plan": "heroku-postgresql:mini"
|
||||
}
|
||||
],
|
||||
"env": {
|
||||
@@ -43,15 +43,18 @@
|
||||
},
|
||||
"SENDGRID_API_KEY": {
|
||||
"description": "Sendgrid api key. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"SENDGRID_SYNC_API_KEY": {
|
||||
"description": "Sendgrid internal sync service",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"SENDGRID_EMAIL": {
|
||||
"description": "Sendgrid email. Used for email reminders in workflows",
|
||||
"value": ""
|
||||
"value": "",
|
||||
"required": false
|
||||
},
|
||||
"EMAIL_FROM": {
|
||||
"description": "Configures the global From: header whilst sending emails",
|
||||
|
||||
@@ -1,81 +1 @@
|
||||
<!-- PROJECT LOGO -->
|
||||
<div align="right">
|
||||
<a href="https://github.com/calcom/cal.com">
|
||||
<img src="https://user-images.githubusercontent.com/8019099/133430653-24422d2a-3c8d-4052-9ad6-0580597151ee.png" alt="Logo">
|
||||
</a>
|
||||
<a href="https://cal.com">Website</a>
|
||||
·
|
||||
<a href="https://github.com/calcom/cal.com/issues">Community Support</a>
|
||||
</div>
|
||||
|
||||
# Cal.com Documentation
|
||||
|
||||
The official product, support and developer documentation, containing information and guides about using the product as well as support for self-hosted installations. This documentation site runs on [Nextra](https://nextra.vercel.app), so you may refer to their documentation should you need information on anything that isn't covered here.
|
||||
|
||||
## Prerequisites
|
||||
|
||||
- Git
|
||||
- Node.js & npm
|
||||
- Yarn
|
||||
|
||||
## Installation
|
||||
|
||||
Firstly, clone the repository using Git:
|
||||
|
||||
```console
|
||||
git clone https://github.com/calcom/docs.git
|
||||
```
|
||||
|
||||
Now, you can install the dependencies with yarn:
|
||||
|
||||
```console
|
||||
yarn install
|
||||
```
|
||||
|
||||
## Editing
|
||||
|
||||
To create, edit and delete documentation pages, you can simply create markdown (.mdx) files in the `pages/` folder. You can edit Markdown with any text editor, but VS Code and WebStorm have side-by-side previews so you can see your formatted content whilst writing markdown.
|
||||
|
||||
You will also need to add it as an entry to the `meta.json` file found in whichever directory that the .mdx file is in.
|
||||
|
||||
## Local Development
|
||||
|
||||
```console
|
||||
yarn dev
|
||||
```
|
||||
|
||||
This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server.
|
||||
|
||||
## Build
|
||||
|
||||
```console
|
||||
yarn build
|
||||
```
|
||||
|
||||
This command generates static content into the `build` directory and can be served using any static content hosting service.
|
||||
|
||||
## How to easily contribute
|
||||
|
||||
## Existing Page
|
||||
|
||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
||||
2. From here you can view all current pages on the documentation site. Select the page you would like to contribute to.
|
||||
3. You should now be able to view the page you have selected. Located at the top right of the page will be a pencil icon. Pressing this will bring you up an editor to edit and make changes. You can add formatting using the buttons at the top, which will automatically insert the relevant markdown content needed to style the text.
|
||||
4. From here make the changes you wish to make.
|
||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
||||
6. Your changes have been saved, to submit them for review, located on your screen, press the green 'Create Pull Request' button.
|
||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
||||
|
||||
You have now successfully edited and submitted changes to our documentation site.
|
||||
|
||||
## Creating a New Page
|
||||
|
||||
1. From the documentation's GitHub repository, head to the folder called 'pages' and open it.
|
||||
2. From here you can view all current pages on the documentation site. At the top of your screen press the 'New file' button.
|
||||
3. You should now be able to view the page you have created. Remember when renaming the document to put .mdx at the end of the file name.
|
||||
4. From here make the changes you wish to make. Such as creating a title, sub-title and body text.
|
||||
5. At the bottom of the screen will be a 'Propose Changes' box, fill in all the relevant details such as title and description then press the green 'Propose Changes' button.
|
||||
6. Your changes have been saved, to submit them for review, located on your screen, press the greem 'Create Pull Request' button.
|
||||
7. Fill in all the relevant details such as title and description and after finalize the submission.
|
||||
|
||||
You have now successfully created and submitted changes to our documentation site.
|
||||
|
||||
@@ -0,0 +1,12 @@
|
||||
import { create } from "@storybook/theming";
|
||||
|
||||
export default create({
|
||||
base: "light",
|
||||
colorPrimary: "#111827",
|
||||
colorSecondary: "#111827",
|
||||
appContentBg: "#F9FAFC",
|
||||
brandTitle: "Cal.com UI",
|
||||
brandUrl: "https://cal.com",
|
||||
brandImage: "https://cal.com/logo.svg",
|
||||
brandTarget: "_self",
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { addons } from "@storybook/addons";
|
||||
|
||||
import yourTheme from "./YourTheme";
|
||||
|
||||
addons.setConfig({
|
||||
theme: yourTheme,
|
||||
});
|
||||
@@ -49,7 +49,7 @@
|
||||
"storybook-addon-next": "^1.6.9",
|
||||
"storybook-react-i18next": "^1.1.2",
|
||||
"tailwindcss": "^3.2.1",
|
||||
"typescript": "^4.7.4",
|
||||
"typescript": "^4.9.4",
|
||||
"vite": "^2.9.15"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,6 +24,6 @@
|
||||
"@types/node": "16.9.1",
|
||||
"@types/react": "^18.0.17",
|
||||
"@types/react-dom": "^18.0.6",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function AddToHomescreen() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<Icon.FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
<FiX className="h-6 w-6 text-white" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,35 +1,92 @@
|
||||
import { ReactNode } from "react";
|
||||
import { useRouter } from "next/router";
|
||||
import { ReactNode, useEffect, useRef, useState } from "react";
|
||||
import { z } from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Badge, ListItemText } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
interface AppListCardProps {
|
||||
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||
|
||||
type AppListCardProps = {
|
||||
logo?: string;
|
||||
title: string;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
isDefault?: boolean;
|
||||
}
|
||||
isTemplate?: boolean;
|
||||
invalidCredential?: boolean;
|
||||
children?: ReactNode;
|
||||
} & ShouldHighlight;
|
||||
|
||||
const schema = z.object({ hl: z.string().optional() });
|
||||
|
||||
export default function AppListCard(props: AppListCardProps) {
|
||||
const { t } = useLocale();
|
||||
const { logo, title, description, actions, isDefault } = props;
|
||||
const {
|
||||
logo,
|
||||
title,
|
||||
description,
|
||||
actions,
|
||||
isDefault,
|
||||
slug,
|
||||
shouldHighlight,
|
||||
isTemplate,
|
||||
invalidCredential,
|
||||
children,
|
||||
} = props;
|
||||
const {
|
||||
data: { hl },
|
||||
} = useTypedQuery(schema);
|
||||
const router = useRouter();
|
||||
const [highlight, setHighlight] = useState(shouldHighlight && hl === slug);
|
||||
const timeoutRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
if (shouldHighlight && highlight) {
|
||||
const timer = setTimeout(() => {
|
||||
setHighlight(false);
|
||||
const url = new URL(window.location.href);
|
||||
url.searchParams.delete("hl");
|
||||
router.replace(url.pathname, undefined, { shallow: true });
|
||||
}, 3000);
|
||||
timeoutRef.current = timer;
|
||||
}
|
||||
return () => {
|
||||
if (timeoutRef.current) {
|
||||
clearTimeout(timeoutRef.current);
|
||||
timeoutRef.current = null;
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-x-3">
|
||||
<div className={`p-4 ${highlight ? "bg-yellow-100" : ""}`}>
|
||||
<div className="flex gap-x-3">
|
||||
{logo ? <img className="h-10 w-10" src={logo} alt={`${title} logo`} /> : null}
|
||||
|
||||
<div className="flex grow flex-col gap-y-1 truncate">
|
||||
<div className="flex items-center gap-x-2">
|
||||
<h3 className="truncate text-sm font-semibold text-gray-900">{title}</h3>
|
||||
{isDefault ? <Badge variant="green">{t("default")}</Badge> : null}
|
||||
<div className="flex items-center gap-x-2">
|
||||
{isDefault && <Badge variant="green">{t("default")}</Badge>}
|
||||
{isTemplate && <Badge variant="red">Template</Badge>}
|
||||
</div>
|
||||
</div>
|
||||
<ListItemText component="p">{description}</ListItemText>
|
||||
{invalidCredential && (
|
||||
<div className="flex gap-x-2 pt-2">
|
||||
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{actions}
|
||||
</div>
|
||||
{children && <div className="w-full">{children}</div>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
InputLeading,
|
||||
Label,
|
||||
showToast,
|
||||
@@ -20,6 +19,7 @@ import {
|
||||
TextArea,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { FiCode, FiTrello, FiSun, FiArrowLeft, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import ColorPicker from "@components/ui/colorpicker";
|
||||
import Select from "@components/ui/form/Select";
|
||||
@@ -488,7 +488,7 @@ const tabs = [
|
||||
{
|
||||
name: "HTML",
|
||||
href: "embedTabName=embed-code",
|
||||
icon: Icon.FiCode,
|
||||
icon: FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -541,7 +541,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
{
|
||||
name: "React",
|
||||
href: "embedTabName=embed-react",
|
||||
icon: Icon.FiCode,
|
||||
icon: FiCode,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -581,7 +581,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
||||
{
|
||||
name: "Preview",
|
||||
href: "embedTabName=embed-preview",
|
||||
icon: Icon.FiTrello,
|
||||
icon: FiTrello,
|
||||
type: "iframe",
|
||||
Component: forwardRef<
|
||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||
@@ -617,7 +617,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||
return (
|
||||
<components.Control {...props}>
|
||||
<Icon.FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||
<FiSun className="ml-2 h-4 w-4 text-gray-500" />
|
||||
{children}
|
||||
</components.Control>
|
||||
);
|
||||
@@ -815,7 +815,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
onClick={() => {
|
||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||
}}>
|
||||
<Icon.FiArrowLeft className="mr-4 w-4" />
|
||||
<FiArrowLeft className="mr-4 w-4" />
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
@@ -835,7 +835,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
? "Floating Popup Customization"
|
||||
: "Element Click Customization"}
|
||||
</div>
|
||||
<Icon.FiChevronRight
|
||||
<FiChevronRight
|
||||
className={`${
|
||||
isEmbedCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-gray-500`}
|
||||
@@ -863,7 +863,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>W</InputLeading>}
|
||||
addOnLeading={<>W</>}
|
||||
/>
|
||||
<span className="p-2">×</span>
|
||||
<TextField
|
||||
@@ -883,7 +883,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
};
|
||||
});
|
||||
}}
|
||||
addOnLeading={<InputLeading>H</InputLeading>}
|
||||
addOnLeading={<>H</>}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
@@ -1002,7 +1002,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
onOpenChange={() => setIsBookingCustomizationOpen((val) => !val)}>
|
||||
<CollapsibleTrigger className="flex w-full" type="button">
|
||||
<div className="text-base font-medium text-gray-900">Cal Booking Customization</div>
|
||||
<Icon.FiChevronRight
|
||||
<FiChevronRight
|
||||
className={`${
|
||||
isBookingCustomizationOpen ? "rotate-90 transform" : ""
|
||||
} ml-auto h-5 w-5 text-gray-500`}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { ComponentProps } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { ErrorBoundary, Icon } from "@calcom/ui";
|
||||
import { ErrorBoundary } from "@calcom/ui";
|
||||
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
||||
|
||||
import NavTabs from "./NavTabs";
|
||||
|
||||
@@ -9,32 +10,32 @@ const tabs = [
|
||||
{
|
||||
name: "profile",
|
||||
href: "/settings/profile",
|
||||
icon: Icon.FiUser,
|
||||
icon: FiUser,
|
||||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
icon: Icon.FiUsers,
|
||||
icon: FiUsers,
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
href: "/settings/security",
|
||||
icon: Icon.FiKey,
|
||||
icon: FiKey,
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
href: "/settings/developer",
|
||||
icon: Icon.FiTerminal,
|
||||
icon: FiTerminal,
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: Icon.FiCreditCard,
|
||||
icon: FiCreditCard,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
href: "/settings/admin",
|
||||
icon: Icon.FiLock,
|
||||
icon: FiLock,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -4,7 +4,8 @@ import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App } from "@calcom/types/App";
|
||||
import { Button, Icon, Select } from "@calcom/ui";
|
||||
import { Button, Select } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -31,7 +32,7 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
||||
/>
|
||||
) : (
|
||||
<Button className="w-full" color="minimal" href="/apps/categories/calendar">
|
||||
<Icon.FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||
<FiPlus className="text-color mr-3 ml-1 h-4 w-4" />
|
||||
<p>{t("install_new_calendar_app")}</p>
|
||||
</Button>
|
||||
);
|
||||
@@ -61,7 +62,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
<Select
|
||||
name="additionalCalendar"
|
||||
placeholder={
|
||||
<Button StartIcon={Icon.FiPlus} color="secondary">
|
||||
<Button StartIcon={FiPlus} color="secondary">
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -12,7 +12,17 @@ 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, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||
import {
|
||||
FiBookOpen,
|
||||
FiCheck,
|
||||
FiExternalLink,
|
||||
FiFile,
|
||||
FiFlag,
|
||||
FiMail,
|
||||
FiPlus,
|
||||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
const Component = ({
|
||||
name,
|
||||
@@ -118,7 +128,7 @@ const Component = ({
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={Icon.FiCheck} color="secondary" disabled>
|
||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("default")}
|
||||
@@ -139,7 +149,7 @@ const Component = ({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
{...props}
|
||||
// @TODO: Overriding color and size prevent us from
|
||||
// having to duplicate InstallAppButton for now.
|
||||
@@ -200,7 +210,7 @@ const Component = ({
|
||||
</span>
|
||||
)}
|
||||
|
||||
<div className="prose prose-sm mt-8">{body}</div>
|
||||
<div className="prose-sm prose mt-8">{body}</div>
|
||||
<h4 className="mt-8 font-semibold text-gray-900 ">{t("pricing")}</h4>
|
||||
<span>
|
||||
{price === 0 ? (
|
||||
@@ -226,7 +236,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-sm font-normal text-black no-underline hover:underline"
|
||||
href={docs}>
|
||||
<Icon.FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
||||
<FiBookOpen className="mr-1 -mt-1 inline h-4 w-4 text-gray-500" />
|
||||
{t("documentation")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -238,7 +248,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={website}>
|
||||
<Icon.FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiExternalLink className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{website.replace("https://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -250,7 +260,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={"mailto:" + email}>
|
||||
<Icon.FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiMail className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
|
||||
{email}
|
||||
</a>
|
||||
@@ -263,7 +273,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={tos}>
|
||||
<Icon.FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiFile className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{t("terms_of_service")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -275,7 +285,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="font-normal text-black no-underline hover:underline"
|
||||
href={privacy}>
|
||||
<Icon.FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
<FiShield className="mr-1 -mt-px inline h-4 w-4 text-gray-500" />
|
||||
{t("privacy_policy")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -286,7 +296,7 @@ const Component = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<Icon.FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
@@ -321,15 +331,11 @@ export default function App(props: {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Shell large isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
||||
<Shell smallHeading isPublic heading={t("app_store")} backPath="/apps" withoutSeo>
|
||||
<HeadSeo
|
||||
title={props.name}
|
||||
description={props.description}
|
||||
app={{ slug: props.logo, name: props.name, description: props.description }}
|
||||
nextSeoProps={{
|
||||
nofollow: true,
|
||||
noindex: true,
|
||||
}}
|
||||
/>
|
||||
{props.licenseRequired ? (
|
||||
<LicenseRequired>
|
||||
|
||||
@@ -11,18 +11,18 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
List,
|
||||
showToast,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
Switch,
|
||||
ShellSubHeading,
|
||||
} from "@calcom/ui";
|
||||
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||
|
||||
type Props = {
|
||||
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
|
||||
/>
|
||||
{!!props.destination && (
|
||||
<span className="ml-4 inline-flex items-center gap-1 rounded-md bg-gray-100 px-2 py-1 text-sm font-normal text-gray-800">
|
||||
<Icon.FiArrowLeft className="h-4 w-4" />
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
{t("adding_events_to")}
|
||||
</span>
|
||||
)}
|
||||
@@ -118,13 +118,13 @@ function CalendarList(props: Props) {
|
||||
success={({ data }) => (
|
||||
<List>
|
||||
{data.items.map((item) => (
|
||||
<IntegrationListItem
|
||||
name={item.name}
|
||||
slug={item.slug}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
<AppListCard
|
||||
title={item.name}
|
||||
key={item.name}
|
||||
logo={item.logo}
|
||||
description={item.description}
|
||||
shouldHighlight
|
||||
slug={item.slug}
|
||||
actions={
|
||||
<InstallAppButton
|
||||
type={item.type}
|
||||
@@ -161,16 +161,16 @@ function ConnectedCalendarsList(props: Props) {
|
||||
}
|
||||
|
||||
return (
|
||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||
<List>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.calendars ? (
|
||||
<IntegrationListItem
|
||||
<AppListCard
|
||||
shouldHighlight
|
||||
slug={item.integration.slug}
|
||||
title={item.integration.title}
|
||||
title={item.integration.name}
|
||||
logo={item.integration.logo}
|
||||
description={item.primary?.email ?? item.integration.description}
|
||||
separate={true}
|
||||
actions={
|
||||
<div className="flex w-32 justify-end">
|
||||
<DisconnectIntegration
|
||||
@@ -200,7 +200,7 @@ function ConnectedCalendarsList(props: Props) {
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</IntegrationListItem>
|
||||
</AppListCard>
|
||||
) : (
|
||||
<Alert
|
||||
severity="warning"
|
||||
@@ -282,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
<div className="flex justify-between rounded-md border border-gray-200 bg-gray-50 p-4">
|
||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||
<div className="relative rounded-md border border-gray-200 bg-white p-1.5">
|
||||
<Icon.FiCalendar className="h-8 w-8" strokeWidth="1" />
|
||||
<Icon.FiPlus
|
||||
<FiCalendar className="h-8 w-8" strokeWidth="1" />
|
||||
<FiPlus
|
||||
className="absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2 text-black"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiCalendar}
|
||||
Icon={FiCalendar}
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
|
||||
@@ -1,80 +0,0 @@
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { ReactNode, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Badge, Icon, ListItem, ListItemText, ListItemTitle, showToast } from "@calcom/ui";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
function IntegrationListItem(props: {
|
||||
imageSrc?: string;
|
||||
slug: string;
|
||||
name?: string;
|
||||
title?: string;
|
||||
description: string;
|
||||
actions?: ReactNode;
|
||||
children?: ReactNode;
|
||||
logo: string;
|
||||
destination?: boolean;
|
||||
separate?: boolean;
|
||||
invalidCredential?: boolean;
|
||||
isTemplate?: boolean;
|
||||
}): JSX.Element {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { hl } = router.query;
|
||||
const [highlight, setHighlight] = useState(hl === props.slug);
|
||||
const title = props.name || props.title;
|
||||
|
||||
// The highlight is to show a newly installed app, coming from the app's
|
||||
// redirection after installation, so we proceed to show the corresponding
|
||||
// message
|
||||
if (highlight) {
|
||||
showToast(t("app_successfully_installed"), "success");
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
const timer = setTimeout(() => setHighlight(false), 3000);
|
||||
return () => {
|
||||
clearTimeout(timer);
|
||||
};
|
||||
}, []);
|
||||
return (
|
||||
<ListItem
|
||||
expanded={!!props.children}
|
||||
className={classNames(
|
||||
props.separate ? "rounded-md" : "first:rounded-t-md last:rounded-b-md",
|
||||
"my-0 flex-col border transition-colors duration-500",
|
||||
highlight ? "bg-yellow-100" : ""
|
||||
)}>
|
||||
<div className={classNames("flex w-full flex-1 items-center space-x-2 p-4 rtl:space-x-reverse")}>
|
||||
{props.logo && <img className="h-11 w-11" src={props.logo} alt={title} />}
|
||||
<div className="flex-grow truncate pl-2">
|
||||
<ListItemTitle component="h3" className="flex ">
|
||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
||||
{props.isTemplate && (
|
||||
<Badge variant="red" className="ml-4">
|
||||
Template
|
||||
</Badge>
|
||||
)}
|
||||
</ListItemTitle>
|
||||
<ListItemText component="p">{props.description}</ListItemText>
|
||||
{/* Alert error that key stopped working. */}
|
||||
{props.invalidCredential && (
|
||||
<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")}
|
||||
</ListItemText>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<div>{props.actions}</div>
|
||||
</div>
|
||||
{props.children && <div className="w-full">{props.children}</div>}
|
||||
</ListItem>
|
||||
);
|
||||
}
|
||||
|
||||
export default IntegrationListItem;
|
||||
@@ -4,7 +4,8 @@ import React, { ComponentProps } from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen, Icon } from "@calcom/ui";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -25,7 +26,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
<main className="w-full">
|
||||
{emptyStore ? (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiAlertCircle}
|
||||
Icon={FiAlertCircle}
|
||||
headline={t("no_apps")}
|
||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||
|
||||
@@ -4,39 +4,39 @@ import AppCategoryNavigation from "@calcom/app-store/_components/AppCategoryNavi
|
||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
|
||||
|
||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||
{
|
||||
name: "calendar",
|
||||
href: "/apps/installed/calendar",
|
||||
icon: Icon.FiCalendar,
|
||||
icon: FiCalendar,
|
||||
},
|
||||
{
|
||||
name: "conferencing",
|
||||
href: "/apps/installed/conferencing",
|
||||
icon: Icon.FiVideo,
|
||||
icon: FiVideo,
|
||||
},
|
||||
{
|
||||
name: "payment",
|
||||
href: "/apps/installed/payment",
|
||||
icon: Icon.FiCreditCard,
|
||||
icon: FiCreditCard,
|
||||
},
|
||||
{
|
||||
name: "automation",
|
||||
href: "/apps/installed/automation",
|
||||
icon: Icon.FiShare2,
|
||||
icon: FiShare2,
|
||||
},
|
||||
{
|
||||
name: "analytics",
|
||||
href: "/apps/installed/analytics",
|
||||
icon: Icon.FiBarChart,
|
||||
icon: FiBarChart,
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
href: "/apps/installed/other",
|
||||
icon: Icon.FiGrid,
|
||||
icon: FiGrid,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -57,9 +57,7 @@ export default function InstalledAppsLayout({
|
||||
|
||||
return (
|
||||
<Shell {...rest}>
|
||||
<AppCategoryNavigation
|
||||
baseURL="/apps/installed"
|
||||
containerClassname="w-full xl:mx-5 xl:w-4/5 xl:max-w-2xl xl:pr-5">
|
||||
<AppCategoryNavigation baseURL="/apps/installed" containerClassname="min-w-0 w-full">
|
||||
{children}
|
||||
</AppCategoryNavigation>
|
||||
</Shell>
|
||||
|
||||
@@ -10,6 +10,7 @@ import { UserPermissionRole } from ".prisma/client";
|
||||
|
||||
export default function AdminLayout({
|
||||
children,
|
||||
|
||||
...rest
|
||||
}: { children: React.ReactNode } & ComponentProps<typeof Shell>) {
|
||||
const session = useSession();
|
||||
@@ -22,10 +23,11 @@ export default function AdminLayout({
|
||||
}
|
||||
}, [session, router]);
|
||||
|
||||
const isAppsPage = router.asPath.startsWith("/settings/admin/apps");
|
||||
return (
|
||||
<SettingsLayout {...rest}>
|
||||
<div className="mx-auto flex max-w-4xl flex-row divide-y divide-gray-200">
|
||||
<div className="flex flex-1 [&>*]:flex-1">
|
||||
<div className={isAppsPage ? "min-w-0" : "flex flex-1 [&>*]:flex-1"}>
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
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 { Tooltip } from "@calcom/ui";
|
||||
import { FiLink } from "@calcom/ui/components/icon";
|
||||
|
||||
import { Props } from "./pages/AvailabilityPage";
|
||||
|
||||
@@ -9,23 +10,23 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
const { t } = useLocale();
|
||||
|
||||
return locations.length ? (
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||
{locations.map((location) => {
|
||||
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-4 break-words text-sm text-gray-600">
|
||||
{locations.map((location, index) => {
|
||||
const eventLocationType = getEventLocationType(location.type);
|
||||
if (!eventLocationType) {
|
||||
// It's possible that the location app got uninstalled
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||
<Icon.FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
<FiLink className="dark:text-darkgray-600 ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 " />
|
||||
) : (
|
||||
<img
|
||||
src={eventLocationType.iconUrl}
|
||||
className={classNames(
|
||||
"ml-[2px] h-4 w-4 opacity-70 ltr:mr-[10px] rtl:ml-[10px] dark:opacity-100 ",
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert-[.65]" : ""
|
||||
)}
|
||||
alt={`${eventLocationType.label} icon`}
|
||||
/>
|
||||
|
||||
@@ -4,7 +4,8 @@ import { FC, ReactNode, useEffect } from "react";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, Badge } from "@calcom/ui";
|
||||
import { Badge } from "@calcom/ui";
|
||||
import { FiCheckSquare, FiClock, FiInfo } from "@calcom/ui/components/icon";
|
||||
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
|
||||
@@ -93,7 +94,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiInfo
|
||||
<FiInfo
|
||||
className={classNames(
|
||||
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "dark:text-darkgray-600 -mt-1 text-gray-500"
|
||||
@@ -112,7 +113,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-sm font-medium text-gray-600"
|
||||
)}>
|
||||
<div>
|
||||
<Icon.FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
</div>
|
||||
{requiresConfirmationText}
|
||||
</div>
|
||||
@@ -126,7 +127,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "dark:text-darkgray-600 text-gray-600",
|
||||
!eventType.metadata?.multipleDuration && "items-center"
|
||||
)}>
|
||||
<Icon.FiClock
|
||||
<FiClock
|
||||
className={classNames(
|
||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||
isBookingPage && "mt-[2px]"
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
MeetingTimeInTimezones,
|
||||
showToast,
|
||||
TextArea,
|
||||
@@ -26,6 +25,7 @@ import {
|
||||
ActionType,
|
||||
TableActions,
|
||||
} from "@calcom/ui";
|
||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
@@ -53,7 +53,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data.status === BookingStatus.REJECTED) {
|
||||
if (data?.status === BookingStatus.REJECTED) {
|
||||
setRejectionDialogIsOpen(false);
|
||||
showToast(t("booking_rejection_success"), "success");
|
||||
} else {
|
||||
@@ -100,7 +100,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: Icon.FiSlash,
|
||||
icon: FiSlash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
{
|
||||
@@ -109,7 +109,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: Icon.FiCheck,
|
||||
icon: FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
color: "primary",
|
||||
},
|
||||
@@ -135,7 +135,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
href: `/booking/${booking.uid}?cancel=true${
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}`,
|
||||
icon: Icon.FiX,
|
||||
icon: FiX,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
@@ -143,13 +143,13 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: Icon.FiClock,
|
||||
icon: FiClock,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}`,
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: Icon.FiSend,
|
||||
icon: FiSend,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
@@ -162,7 +162,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: Icon.FiMapPin,
|
||||
icon: FiMapPin,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -179,7 +179,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const RequestSentMessage = () => {
|
||||
return (
|
||||
<div className="ml-1 mr-8 flex text-gray-500" data-testid="request_reschedule_sent">
|
||||
<Icon.FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<FiSend className="-mt-[1px] w-4 rotate-45" />
|
||||
<p className="ml-2 ">{t("reschedule_request_sent")}</p>
|
||||
</div>
|
||||
);
|
||||
@@ -455,7 +455,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||
);
|
||||
})}>
|
||||
<div className="text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCcw
|
||||
<FiRefreshCcw
|
||||
strokeWidth="3"
|
||||
className="float-left mr-1 mt-1.5 inline-block h-3 w-3 text-gray-400"
|
||||
/>
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, Icon, TextArea } from "@calcom/ui";
|
||||
import { Button, TextArea } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -28,7 +29,7 @@ export default function CancelBooking(props: Props) {
|
||||
const [cancellationReason, setCancellationReason] = useState<string>("");
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const { booking, profile, team, allRemainingBookings } = props;
|
||||
const { booking, allRemainingBookings } = props;
|
||||
const [loading, setLoading] = useState(false);
|
||||
const telemetry = useTelemetry();
|
||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||
@@ -39,7 +40,7 @@ export default function CancelBooking(props: Props) {
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
<FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { FC, useEffect, useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { ITimezoneOption, TimezoneSelect } from "@calcom/ui";
|
||||
|
||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||
|
||||
import { timeZone } from "../../lib/clock";
|
||||
|
||||
type Props = {
|
||||
@@ -11,7 +13,8 @@ type Props = {
|
||||
|
||||
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||
const { t } = useLocale();
|
||||
const query = useMeQuery();
|
||||
const userTheme = useTheme(query?.data?.theme).resolvedTheme;
|
||||
|
||||
useEffect(() => {
|
||||
setSelectedTimeZone(timeZone());
|
||||
@@ -23,18 +26,60 @@ const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||
}
|
||||
}, [selectedTimeZone, onSelectTimeZone]);
|
||||
|
||||
return selectedTimeZone !== "" ? (
|
||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
||||
<div className="mb-4 flex">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
</div>
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||
className="focus:border-brand mt-1 mb-2 block w-full rounded-md border-gray-300 text-sm focus:ring-black"
|
||||
/>
|
||||
</div>
|
||||
const customStyles = {
|
||||
option: (_provided: object, state: { isSelected: boolean; isFocused: boolean }) => ({
|
||||
padding: "10px 12px !important",
|
||||
color: state.isSelected || state.isFocused ? "#101010" : "#374151",
|
||||
backgroundColor: state.isSelected ? "#E5E7EB !important" : "white",
|
||||
...(userTheme === "dark" && {
|
||||
color: state.isSelected || state.isFocused ? "white" : "#80868B",
|
||||
}),
|
||||
}),
|
||||
control: () => ({
|
||||
display: "flex",
|
||||
cursor: "pointer",
|
||||
backgroundColor: "transparent !important",
|
||||
minWidth: "5rem",
|
||||
height: "24px",
|
||||
minHeight: "24px !important",
|
||||
boxShadow: "none !important",
|
||||
}),
|
||||
singleValue: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||
...provided,
|
||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||
...(userTheme === "dark" && {
|
||||
color: "#a5a5a5 !important",
|
||||
}),
|
||||
}),
|
||||
menu: (provided: object) => ({
|
||||
...provided,
|
||||
minWidth: "20rem",
|
||||
margin: "8px 0 0 -20px",
|
||||
}),
|
||||
valueContainer: (provided: object) => ({
|
||||
...provided,
|
||||
padding: "0 0 0 6px",
|
||||
}),
|
||||
dropdownIndicator: (provided: object, state: { selectProps: { menuIsOpen: boolean } }) => ({
|
||||
...provided,
|
||||
transform: state.selectProps.menuIsOpen ? "rotate(180deg)" : "",
|
||||
color: state.selectProps.menuIsOpen ? "#111827" : "#4B5563",
|
||||
marginLeft: 4,
|
||||
padding: 0,
|
||||
...(userTheme === "dark" && {
|
||||
color: "#80868B",
|
||||
}),
|
||||
}),
|
||||
};
|
||||
|
||||
return !!selectedTimeZone ? (
|
||||
<TimezoneSelect
|
||||
id="timeZone"
|
||||
value={selectedTimeZone}
|
||||
onChange={(tz: ITimezoneOption) => setSelectedTimeZone(tz.value)}
|
||||
className="flex h-6 text-sm font-medium"
|
||||
styles={customStyles}
|
||||
/>
|
||||
) : null;
|
||||
};
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { EventType } from "@prisma/client";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { useRouter } from "next/router";
|
||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||
import { Toaster } from "react-hot-toast";
|
||||
@@ -20,15 +19,17 @@ import {
|
||||
import DatePicker from "@calcom/features/calendars/DatePicker";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import notEmpty from "@calcom/lib/notEmpty";
|
||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon, HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiCreditCard, FiGlobe, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -97,7 +98,10 @@ const SlotPicker = ({
|
||||
weekStart = 0,
|
||||
ethSignature,
|
||||
}: {
|
||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
||||
eventType: Pick<
|
||||
EventType & { metadata: z.infer<typeof EventTypeMetaDataSchema> },
|
||||
"id" | "schedulingType" | "slug" | "length" | "metadata"
|
||||
>;
|
||||
timeFormat: TimeFormat;
|
||||
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||
timeZone?: string;
|
||||
@@ -109,11 +113,15 @@ const SlotPicker = ({
|
||||
}) => {
|
||||
const [selectedDate, setSelectedDate] = useState<Dayjs>();
|
||||
const [browsingDate, setBrowsingDate] = useState<Dayjs>();
|
||||
const { duration } = useRouterQuery("duration");
|
||||
let { duration = eventType.length.toString() } = useRouterQuery("duration");
|
||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||
const router = useRouter();
|
||||
|
||||
if (!eventType.metadata?.multipleDuration) {
|
||||
duration = eventType.length.toString();
|
||||
}
|
||||
|
||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
useEffect(() => {
|
||||
@@ -203,41 +211,22 @@ const SlotPicker = ({
|
||||
|
||||
function TimezoneDropdown({
|
||||
onChangeTimeZone,
|
||||
timeZone,
|
||||
}: {
|
||||
onChangeTimeZone: (newTimeZone: string) => void;
|
||||
timeZone?: string;
|
||||
}) {
|
||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||
|
||||
const handleSelectTimeZone = (newTimeZone: string) => {
|
||||
onChangeTimeZone(newTimeZone);
|
||||
localStorageTimeZone(newTimeZone);
|
||||
setIsTimeOptionsOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 !mt-2 inline-block self-start rounded-md px-2 py-2 text-left text-gray-600">
|
||||
<p className="flex items-center text-sm font-medium">
|
||||
<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" />
|
||||
) : (
|
||||
<Icon.FiChevronDown className="min-h-4 min-w-4 ml-1 inline-block" />
|
||||
)}
|
||||
</p>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
<>
|
||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 -mx-[2px] !mt-3 flex w-fit items-center rounded-[4px] px-1 py-[2px] text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white">
|
||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-[2px] rtl:ml-[2px]" />
|
||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -302,7 +291,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||
[timeZone]
|
||||
);
|
||||
const stripeAppData = getStripeAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
||||
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||
@@ -353,10 +342,10 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
isBackgroundTransparent
|
||||
? ""
|
||||
: "dark:bg-darkgray-100 sm:dark:border-darkgray-300 bg-white pb-4 md:pb-0",
|
||||
"border-bookinglightest overflow-hidden md:rounded-md md:border",
|
||||
"border-bookinglightest md:rounded-md md:border",
|
||||
isEmbed && "mx-auto"
|
||||
)}>
|
||||
<div className="overflow-hidden md:flex">
|
||||
<div className="md:flex">
|
||||
{showEventTypeDetails && (
|
||||
<div
|
||||
className={classNames(
|
||||
@@ -367,7 +356,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 mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||
<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 })}
|
||||
@@ -390,14 +379,14 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{stripeAppData.price > 0 && (
|
||||
{paymentAppData.price > 0 && (
|
||||
<p className="-ml-2 px-2 text-sm font-medium">
|
||||
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
|
||||
@@ -6,9 +6,8 @@ import { useSession } from "next-auth/react";
|
||||
import Head from "next/head";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect, useMemo, useReducer, useState } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { useFieldArray, useForm, useWatch, Controller } from "react-hook-form";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
import { ReactMultiEmail } from "react-multi-email";
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import { z } from "zod";
|
||||
|
||||
@@ -33,14 +32,34 @@ import {
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { AddressInput, Button, EmailInput, Form, Icon, PhoneInput, Tooltip } from "@calcom/ui";
|
||||
import { Group, RadioField } from "@calcom/ui";
|
||||
import {
|
||||
AddressInput,
|
||||
Button,
|
||||
EmailField,
|
||||
EmailInput,
|
||||
Form,
|
||||
Group,
|
||||
PhoneInput,
|
||||
RadioField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiUserPlus,
|
||||
FiCalendar,
|
||||
FiX,
|
||||
FiInfo,
|
||||
FiCreditCard,
|
||||
FiRefreshCw,
|
||||
FiUser,
|
||||
FiAlertTriangle,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||
import { timeZone } from "@lib/clock";
|
||||
@@ -49,7 +68,6 @@ import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import Gates, { Gate, GateState } from "@components/Gates";
|
||||
import BookingDescription from "@components/booking/BookingDescription";
|
||||
@@ -65,7 +83,7 @@ type BookingFormValues = {
|
||||
email: string;
|
||||
notes?: string;
|
||||
locationType?: EventLocationType["type"];
|
||||
guests?: string[];
|
||||
guests?: { email: string }[];
|
||||
address?: string;
|
||||
attendeeAddress?: string;
|
||||
phone?: string;
|
||||
@@ -104,10 +122,15 @@ const BookingPage = ({
|
||||
}),
|
||||
{}
|
||||
);
|
||||
const stripeAppData = getStripeAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
// Define duration now that we support multiple duration eventTypes
|
||||
let duration = eventType.length;
|
||||
if (queryDuration && !isNaN(Number(queryDuration))) {
|
||||
if (
|
||||
queryDuration &&
|
||||
!isNaN(Number(queryDuration)) &&
|
||||
eventType.metadata?.multipleDuration &&
|
||||
eventType.metadata?.multipleDuration.includes(Number(queryDuration))
|
||||
) {
|
||||
duration = Number(queryDuration);
|
||||
}
|
||||
|
||||
@@ -125,6 +148,7 @@ const BookingPage = ({
|
||||
const mutation = useMutation(createBooking, {
|
||||
onSuccess: async (responseData) => {
|
||||
const { uid, paymentUid } = responseData;
|
||||
|
||||
if (paymentUid) {
|
||||
return await router.push(
|
||||
createPaymentLink({
|
||||
@@ -143,7 +167,7 @@ const BookingPage = ({
|
||||
isSuccessBookingPage: true,
|
||||
email: bookingForm.getValues("email"),
|
||||
eventTypeSlug: eventType.slug,
|
||||
formerTime: booking?.startTime.toString(),
|
||||
...(rescheduleUid && booking?.startTime && { formerTime: booking.startTime.toString() }),
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -184,7 +208,9 @@ const BookingPage = ({
|
||||
|
||||
const loggedInIsOwner = eventType?.users[0]?.id === session?.user?.id;
|
||||
const guestListEmails = !isDynamicGroupBooking
|
||||
? booking?.attendees.slice(1).map((attendee) => attendee.email)
|
||||
? booking?.attendees.slice(1).map((attendee) => {
|
||||
return { email: attendee.email };
|
||||
})
|
||||
: [];
|
||||
|
||||
// There should only exists one default userData variable for primaryAttendee.
|
||||
@@ -203,7 +229,9 @@ const BookingPage = ({
|
||||
name: defaultUserValues.name || (!loggedInIsOwner && session?.user?.name) || "",
|
||||
email: defaultUserValues.email || (!loggedInIsOwner && session?.user?.email) || "",
|
||||
notes: (router.query.notes as string) || "",
|
||||
guests: ensureArray(router.query.guest) as string[],
|
||||
guests: ensureArray(router.query.guest).map((guest) => {
|
||||
return { email: guest as string };
|
||||
}),
|
||||
customInputs: eventType.customInputs.reduce(
|
||||
(customInputs, input) => ({
|
||||
...customInputs,
|
||||
@@ -245,6 +273,7 @@ const BookingPage = ({
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
email: z.string().trim().email(),
|
||||
guests: z.array(z.object({ email: z.string().email() })).optional(),
|
||||
phone: z
|
||||
.string()
|
||||
.refine((val) => isValidPhoneNumber(val))
|
||||
@@ -263,6 +292,10 @@ const BookingPage = ({
|
||||
defaultValues: defaultValues(),
|
||||
resolver: zodResolver(bookingFormSchema), // Since this isn't set to strict we only validate the fields in the schema
|
||||
});
|
||||
const guestsField = useFieldArray({
|
||||
name: "guests",
|
||||
control: bookingForm.control,
|
||||
});
|
||||
|
||||
const selectedLocationType = useWatch({
|
||||
control: bookingForm.control,
|
||||
@@ -301,6 +334,7 @@ const BookingPage = ({
|
||||
}
|
||||
|
||||
const bookEvent = (booking: BookingFormValues) => {
|
||||
bookingForm.clearErrors();
|
||||
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] : "",
|
||||
@@ -368,6 +402,29 @@ const BookingPage = ({
|
||||
}
|
||||
}
|
||||
|
||||
// Validate that guests are unique
|
||||
let alreadyInvited = false;
|
||||
booking.guests?.forEach((guest, index) => {
|
||||
if (guest.email === booking.email) {
|
||||
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
||||
alreadyInvited = true;
|
||||
}
|
||||
|
||||
if (booking.guests) {
|
||||
let guestCount = 0;
|
||||
for (const checkGuest of booking.guests) {
|
||||
if (checkGuest.email === guest.email) guestCount++;
|
||||
if (guestCount > 1) {
|
||||
bookingForm.setError(`guests.${index}`, { type: "validate", message: t("already_invited") });
|
||||
alreadyInvited = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
if (alreadyInvited) return;
|
||||
|
||||
if (recurringDates.length) {
|
||||
// Identify set of bookings to one intance of recurring event to support batch changes
|
||||
const recurringEventId = uuidv4();
|
||||
@@ -398,6 +455,7 @@ const BookingPage = ({
|
||||
? booking.phone
|
||||
: booking.smsReminderNumber || undefined,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
guests: booking.guests?.map((guest) => guest.email),
|
||||
}));
|
||||
recurringMutation.mutate(recurringBookings);
|
||||
} else {
|
||||
@@ -426,6 +484,7 @@ const BookingPage = ({
|
||||
? booking.phone
|
||||
: booking.smsReminderNumber || undefined,
|
||||
ethSignature: gateState.rainbowToken,
|
||||
guests: booking.guests?.map((guest) => guest.email),
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -479,7 +538,7 @@ const BookingPage = ({
|
||||
})}{" "}
|
||||
| {APP_NAME}
|
||||
</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
<link rel="icon" href="/favico.ico" />
|
||||
</Head>
|
||||
<BookingPageTagManager eventType={eventType} />
|
||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||
@@ -499,21 +558,21 @@ const BookingPage = ({
|
||||
{showEventTypeDetails && (
|
||||
<div className="sm:dark:border-darkgray-300 dark:text-darkgray-600 flex flex-col px-6 pt-6 pb-0 text-gray-600 sm:w-1/2 sm:border-r sm:pb-6">
|
||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||
{stripeAppData.price > 0 && (
|
||||
{paymentAppData.price > 0 && (
|
||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||
<Icon.FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={stripeAppData.price / 100.0}
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={stripeAppData.currency.toUpperCase()}
|
||||
currency={paymentAppData?.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
</p>
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<div className="items-start text-sm font-medium text-gray-600 dark:text-white">
|
||||
<Icon.FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<p className="-ml-2 inline-block items-center px-2">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
@@ -524,7 +583,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
)}
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<Icon.FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<div className="text-sm font-medium">
|
||||
{(rescheduleUid || !eventType.recurringEvent?.freq) && `${parseDate(date, i18n)}`}
|
||||
{!rescheduleUid &&
|
||||
@@ -552,14 +611,14 @@ const BookingPage = ({
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<p className="line-through ">
|
||||
<Icon.FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{typeof booking.startTime === "string" && parseDate(dayjs(booking.startTime), i18n)}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
<div className="text-bookinghighlight flex items-start text-sm">
|
||||
<Icon.FiUser
|
||||
<FiUser
|
||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
? "text-rose-600"
|
||||
@@ -623,7 +682,7 @@ const BookingPage = ({
|
||||
/>
|
||||
{bookingForm.formState.errors.email && (
|
||||
<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" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("email_validation_error")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -716,7 +775,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
{bookingForm.formState.errors.phone && (
|
||||
<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" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -834,7 +893,7 @@ const BookingPage = ({
|
||||
/>
|
||||
{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" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -842,60 +901,7 @@ const BookingPage = ({
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
{!eventType.disableGuests && guestToggle && (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
{!disableInput && (
|
||||
<Controller
|
||||
control={bookingForm.control}
|
||||
name="guests"
|
||||
render={({ field: { onChange, value } }) => (
|
||||
<ReactMultiEmail
|
||||
className="relative"
|
||||
placeholder={<span className="dark:text-darkgray-600">guest@example.com</span>}
|
||||
emails={value}
|
||||
onChange={onChange}
|
||||
getLabel={(
|
||||
email: string,
|
||||
index: number,
|
||||
removeEmail: (index: number) => void
|
||||
) => {
|
||||
return (
|
||||
<div data-tag key={index} className="cursor-pointer">
|
||||
{email}
|
||||
{!disableInput && (
|
||||
<span data-tag-handle onClick={() => removeEmail(index)}>
|
||||
×
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
)}
|
||||
{/* Custom code when guest emails should not be editable */}
|
||||
{disableInput && guestListEmails && guestListEmails.length > 0 && (
|
||||
<div data-tag className="react-multi-email">
|
||||
{/* // @TODO: user owners are appearing as guest here when should be only user input */}
|
||||
{guestListEmails.map((email, index) => {
|
||||
return (
|
||||
<div key={index} className="cursor-pointer">
|
||||
<span data-tag>{email}</span>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{isSmsReminderNumberNeeded && selectedLocationType !== LocationType.Phone && (
|
||||
<div className="mb-4">
|
||||
<label
|
||||
@@ -914,7 +920,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
{bookingForm.formState.errors.smsReminderNumber && (
|
||||
<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" />
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p>{t("invalid_number")}</p>
|
||||
</div>
|
||||
)}
|
||||
@@ -948,19 +954,84 @@ const BookingPage = ({
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
{!eventType.disableGuests && guestsField.fields.length ? (
|
||||
<div className="mb-4">
|
||||
<div>
|
||||
<label
|
||||
htmlFor="guests"
|
||||
className="mb-1 block text-sm font-medium text-gray-700 dark:text-white">
|
||||
{t("guests")}
|
||||
</label>
|
||||
<ul>
|
||||
{guestsField.fields.map((field, index) => (
|
||||
<li key={field.id}>
|
||||
<EmailField
|
||||
{...bookingForm.register(`guests.${index}.email` as const)}
|
||||
className={classNames(
|
||||
inputClassName,
|
||||
bookingForm.formState.errors.guests?.[index] &&
|
||||
"!focus:ring-red-700 !border-red-700",
|
||||
"border-r-0"
|
||||
)}
|
||||
addOnClassname={classNames(
|
||||
"border-gray-300 border block border-l-0 disabled:bg-gray-200 disabled:hover:cursor-not-allowed bg-transparent disabled:text-gray-500 dark:border-darkgray-300 ",
|
||||
bookingForm.formState.errors.guests?.[index] &&
|
||||
"!focus:ring-red-700 !border-red-700"
|
||||
)}
|
||||
placeholder="guest@example.com"
|
||||
label={<></>}
|
||||
required
|
||||
addOnSuffix={
|
||||
<Tooltip content="Remove guest">
|
||||
<button
|
||||
className="m-1 disabled:hover:cursor-not-allowed"
|
||||
type="button"
|
||||
onClick={() => guestsField.remove(index)}>
|
||||
<FiX className="text-gray-600" />
|
||||
</button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
{bookingForm.formState.errors.guests?.[index] && (
|
||||
<div className="mt-2 flex items-center text-sm text-red-700 ">
|
||||
<FiInfo className="h-3 w-3 ltr:mr-2 rtl:ml-2" />
|
||||
<p className="text-red-700">
|
||||
{bookingForm.formState.errors.guests?.[index]?.message}
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
StartIcon={FiUserPlus}
|
||||
className="my-2.5"
|
||||
// className="mb-1 block text-sm font-medium text-gray-700 dark:text-white"
|
||||
onClick={() => guestsField.append({ email: "" })}>
|
||||
{t("add_another")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<></>
|
||||
)}
|
||||
|
||||
{!eventType.disableGuests && !guestsField.fields.length && (
|
||||
<Button
|
||||
color="minimal"
|
||||
variant="button"
|
||||
StartIcon={FiUserPlus}
|
||||
onClick={() => {
|
||||
guestsField.append({ email: "" });
|
||||
}}
|
||||
className="mr-auto">
|
||||
{t("additional_guests")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<div className="flex justify-end space-x-2 rtl:space-x-reverse">
|
||||
{!eventType.disableGuests && !guestToggle && (
|
||||
<Button
|
||||
type="button"
|
||||
color="minimal"
|
||||
variant="icon"
|
||||
tooltip={t("additional_guests")}
|
||||
StartIcon={Icon.FiUserPlus}
|
||||
onClick={() => setGuestToggle(!guestToggle)}
|
||||
className="mr-auto"
|
||||
/>
|
||||
)}
|
||||
<Button color="minimal" type="button" onClick={() => router.back()}>
|
||||
{t("cancel")}
|
||||
</Button>
|
||||
@@ -993,7 +1064,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-yellow-400 bg-yellow-50 p-4">
|
||||
<div className="flex">
|
||||
<div className="flex-shrink-0">
|
||||
<Icon.FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
<FiAlertTriangle className="h-5 w-5 text-yellow-400" aria-hidden="true" />
|
||||
</div>
|
||||
<div className="ltr:ml-3 rtl:mr-3">
|
||||
<p className="text-sm text-yellow-700">
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import { ErrorMessage } from "@hookform/error-message";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||
import { Trans } from "next-i18next";
|
||||
import Link from "next/link";
|
||||
import { useEffect } from "react";
|
||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -15,7 +17,8 @@ import {
|
||||
} from "@calcom/app-store/locations";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, Icon, PhoneInput } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -204,10 +207,10 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
|
||||
return (
|
||||
<Dialog open={isOpenDialog} onOpenChange={(open) => setShowLocationModal(open)}>
|
||||
<DialogContent disableOverflow>
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="bg-secondary-100 mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||
<Icon.FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
<FiMapPin className="text-primary-600 h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
@@ -215,7 +218,15 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
{t("edit_location")}
|
||||
</h3>
|
||||
{!booking && (
|
||||
<p className="text-sm text-gray-400">{t("this_input_will_shown_booking_this_event")}</p>
|
||||
<p className="text-sm text-gray-400">
|
||||
<Trans i18nKey="cant_find_the_right_video_app_visit_our_app_store">
|
||||
Can't find the right video app? Visit our
|
||||
<Link className="cursor-pointer text-blue-500 underline" href="/apps/categories/video">
|
||||
App Store
|
||||
</Link>
|
||||
.
|
||||
</Trans>
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left" />
|
||||
@@ -288,30 +299,31 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
name="locationType"
|
||||
control={locationFormMethods.control}
|
||||
render={() => (
|
||||
<LocationSelect
|
||||
maxMenuHeight={300}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={locationOptions}
|
||||
isSearchable
|
||||
className="my-4 block w-full min-w-0 flex-1 rounded-sm border border-gray-300 text-sm"
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
<div className="py-4">
|
||||
<LocationSelect
|
||||
maxMenuHeight={300}
|
||||
name="location"
|
||||
defaultValue={selection}
|
||||
options={locationOptions}
|
||||
isSearchable
|
||||
onChange={(val) => {
|
||||
if (val) {
|
||||
locationFormMethods.setValue("locationType", val.value);
|
||||
locationFormMethods.unregister([
|
||||
"locationLink",
|
||||
"locationAddress",
|
||||
"locationPhoneNumber",
|
||||
]);
|
||||
locationFormMethods.clearErrors([
|
||||
"locationLink",
|
||||
"locationPhoneNumber",
|
||||
"locationAddress",
|
||||
]);
|
||||
setSelectedLocation?.(val);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
@@ -325,7 +337,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
setShowLocationModal(false);
|
||||
setSelectedLocation?.(undefined);
|
||||
setEditingLocationType?.("");
|
||||
locationFormMethods.unregister("locationType");
|
||||
locationFormMethods.unregister(["locationType", "locationLink"]);
|
||||
}}
|
||||
type="button"
|
||||
color="secondary">
|
||||
|
||||
@@ -9,10 +9,10 @@ import {
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
DialogHeader,
|
||||
Icon,
|
||||
showToast,
|
||||
TextArea,
|
||||
} from "@calcom/ui";
|
||||
import { FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -43,7 +43,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiClock className="m-auto h-6 w-6" />
|
||||
<FiClock className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -8,7 +8,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Icon, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -98,100 +99,105 @@ const format = (date: Date, hour12: boolean) =>
|
||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||
);
|
||||
|
||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
const EventTypeScheduleDetails = () => {
|
||||
const { data: loggedInUser } = useMeQuery();
|
||||
const timeFormat = loggedInUser?.timeFormat;
|
||||
const { t, i18n } = useLocale();
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const me = useMeQuery();
|
||||
const timeFormat = me?.data?.timeFormat;
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<div className="min-w-4 mb-2">
|
||||
<label htmlFor="availability" className="mt-0 flex text-sm font-medium text-gray-700">
|
||||
{t("availability")}
|
||||
</label>
|
||||
</div>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
||||
<ol className="table border-collapse text-sm">
|
||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||
const isAvailable = !!filterDays(index).length;
|
||||
return (
|
||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||
<span
|
||||
className={classNames(
|
||||
"w-20 font-medium sm:w-32",
|
||||
!isAvailable && "text-gray-500 opacity-50"
|
||||
)}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3 text-right">
|
||||
{filterDays(index).map((dayRange, i) => (
|
||||
<div key={i} className="flex items-center leading-4">
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<Icon.FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${schedule?.schedule.id}`}
|
||||
color="minimal"
|
||||
EndIcon={Icon.FiExternalLink}
|
||||
target="_blank"
|
||||
className="justify-center border sm:border-0"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery({ scheduleId });
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||
);
|
||||
|
||||
const filterDays = (dayNum: number) =>
|
||||
schedule?.schedule.availability.filter((item) => item.days.includes((dayNum + 1) % 7)) || [];
|
||||
|
||||
if (!isTeamEvent) {
|
||||
return <EventTypeSchedule />;
|
||||
}
|
||||
return (
|
||||
<div className="space-y-4 rounded border px-6 pb-4">
|
||||
<ol className="table border-collapse text-sm">
|
||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||
const isAvailable = !!filterDays(index).length;
|
||||
return (
|
||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||
<span
|
||||
className={classNames(
|
||||
"w-20 font-medium sm:w-32",
|
||||
!isAvailable && "text-gray-500 opacity-50"
|
||||
)}>
|
||||
{day}
|
||||
</span>
|
||||
{isLoading ? (
|
||||
<SkeletonText className="block h-5 w-60" />
|
||||
) : isAvailable ? (
|
||||
<div className="space-y-3 text-right">
|
||||
{filterDays(index).map((dayRange, i) => (
|
||||
<div key={i} className="flex items-center leading-4">
|
||||
<span className="w-16 sm:w-28 sm:text-left">
|
||||
{format(dayRange.startTime, timeFormat === 12)}
|
||||
</span>
|
||||
<span className="ltr:ml-4 rtl:mr-4">-</span>
|
||||
<div className="ml-6">{format(dayRange.endTime, timeFormat === 12)}</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||
)}
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
</ol>
|
||||
<hr />
|
||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||
</span>
|
||||
<Button
|
||||
href={`/availability/${schedule?.schedule.id}`}
|
||||
color="minimal"
|
||||
EndIcon={FiExternalLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="availability" className="mb-2 block text-sm font-medium leading-none text-gray-700">
|
||||
{t("availability")}
|
||||
</label>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
<EventTypeScheduleDetails />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const UseCommonScheduleSettingsToggle = () => {
|
||||
const { t } = useLocale();
|
||||
const { resetField, setValue } = useFormContext<FormValues>();
|
||||
return (
|
||||
<Controller
|
||||
name="metadata.config.useHostSchedulesForTeamEvent"
|
||||
@@ -200,6 +206,11 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
checked={!value}
|
||||
onCheckedChange={(checked) => {
|
||||
onChange(!checked);
|
||||
if (checked) {
|
||||
resetField("schedule");
|
||||
} else {
|
||||
setValue("schedule", null);
|
||||
}
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
description={t("choose_common_schedule_team_event_description")}>
|
||||
@@ -209,3 +220,7 @@ export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
||||
};
|
||||
|
||||
@@ -5,7 +5,8 @@ import { FC } from "react";
|
||||
import { Control, Controller, useFieldArray, useForm, UseFormRegister, useWatch } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Label, Select, TextField } from "@calcom/ui";
|
||||
import { Button, Label, Select, TextField } from "@calcom/ui";
|
||||
import { FiPlus, FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
interface OptionTypeBase {
|
||||
label: string;
|
||||
@@ -77,7 +78,7 @@ const CustomInputTypeForm: FC<Props> = (props) => {
|
||||
defaultValue={selectedInputOption}
|
||||
options={inputOptions}
|
||||
isSearchable={false}
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
||||
className="mt-1 mb-2 block w-full min-w-0 flex-1 text-sm"
|
||||
onChange={(option) => option && field.onChange(option.value)}
|
||||
value={selectedInputOption}
|
||||
onBlur={field.onBlur}
|
||||
@@ -185,7 +186,7 @@ function RadioInputHandler({
|
||||
<Button
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiX}
|
||||
StartIcon={FiX}
|
||||
onClick={() => {
|
||||
remove(index);
|
||||
}}
|
||||
@@ -196,7 +197,7 @@ function RadioInputHandler({
|
||||
))}
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
className="!text-sm !font-medium"
|
||||
onClick={() => {
|
||||
append({ label: "", type: "text" });
|
||||
|
||||
@@ -5,6 +5,8 @@ import { Controller, useFormContext } from "react-hook-form";
|
||||
import short from "short-uuid";
|
||||
import { v5 as uuidv5 } from "uuid";
|
||||
|
||||
import type { EventNameObjectType } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||
import CustomInputItem from "@calcom/features/eventtypes/components/CustomInputItem";
|
||||
import { APP_NAME, CAL_URL, IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
@@ -18,13 +20,13 @@ import {
|
||||
DialogClose,
|
||||
DialogContent,
|
||||
DialogFooter,
|
||||
Icon,
|
||||
Label,
|
||||
SettingsToggle,
|
||||
showToast,
|
||||
TextField,
|
||||
Tooltip,
|
||||
} from "@calcom/ui";
|
||||
import { FiEdit, FiCopy, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import CustomInputTypeForm from "@components/eventtype/CustomInputTypeForm";
|
||||
|
||||
@@ -57,6 +59,14 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||
const eventNameObject: EventNameObjectType = {
|
||||
attendeeName: t("scheduler"),
|
||||
eventType: eventType.title,
|
||||
eventName: eventType.eventName,
|
||||
host: eventType.users[0]?.name || "Nameless",
|
||||
t,
|
||||
};
|
||||
const [previewText, setPreviewText] = useState(getEventName(eventNameObject));
|
||||
const [customInputs, setCustomInputs] = useState<CustomInputParsed[]>(
|
||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||
);
|
||||
@@ -64,7 +74,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
|
||||
const removeCustom = (index: number) => {
|
||||
formMethods.getValues("customInputs").splice(index, 1);
|
||||
@@ -72,6 +82,15 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
setCustomInputs([...customInputs]);
|
||||
};
|
||||
|
||||
const changePreviewText = (eventNameObject: EventNameObjectType, previewEventName: string) => {
|
||||
setPreviewText(
|
||||
previewEventName
|
||||
.replace("{Event type title}", eventNameObject.eventType)
|
||||
.replace("{Scheduler}", eventNameObject.attendeeName)
|
||||
.replace("{Organiser}", eventNameObject.host)
|
||||
);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||
}, [eventType.users, hashedUrl, team?.id]);
|
||||
@@ -120,15 +139,22 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
)}
|
||||
<div className="w-full">
|
||||
<TextField
|
||||
label={t("event_name")}
|
||||
label={t("event_name_in_calendar")}
|
||||
type="text"
|
||||
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||
placeholder={t("meeting_with_user")}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventName")}
|
||||
{...formMethods.register("eventName", {
|
||||
onChange: (e) => {
|
||||
if (!e.target.value || !formMethods.getValues("eventName")) {
|
||||
return setPreviewText(getEventName(eventNameObject));
|
||||
}
|
||||
changePreviewText(eventNameObject, e.target.value);
|
||||
},
|
||||
})}
|
||||
addOnSuffix={
|
||||
<Button
|
||||
type="button"
|
||||
StartIcon={Icon.FiEdit}
|
||||
StartIcon={FiEdit}
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
className="hover:stroke-3 min-w-fit px-0 hover:bg-transparent hover:text-black"
|
||||
@@ -168,7 +194,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
</ul>
|
||||
{customInputs.length > 0 && (
|
||||
<Button
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
color="minimal"
|
||||
type="button"
|
||||
onClick={() => {
|
||||
@@ -314,7 +340,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
}}
|
||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||
type="button">
|
||||
<Icon.FiCopy />
|
||||
<FiCopy />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -384,23 +410,63 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("custom_event_name_description")}
|
||||
type="creation">
|
||||
<TextField
|
||||
label={t("event_name")}
|
||||
label={t("event_name_in_calendar")}
|
||||
type="text"
|
||||
placeholder={t("meeting_with_user", { attendeeName: eventType.users[0]?.name })}
|
||||
placeholder={t("meeting_with_user")}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventName")}
|
||||
{...formMethods.register("eventName", {
|
||||
onChange: (e) => {
|
||||
if (!e.target.value || !formMethods.getValues("eventName")) {
|
||||
return setPreviewText(getEventName(eventNameObject));
|
||||
}
|
||||
changePreviewText(eventNameObject, e.target.value);
|
||||
},
|
||||
})}
|
||||
className="mb-0"
|
||||
/>
|
||||
<div className="mt-1 text-gray-500">
|
||||
<p>{`{HOST} = ${t("your_name")}`}</p>
|
||||
<p>{`{ATTENDEE} = ${t("attendee_name")}`}</p>
|
||||
<p>{`{HOST/ATTENDEE} = ${t("dynamically_display_attendee_or_organizer")}`}</p>
|
||||
<p>{`{LOCATION} = ${t("event_location")}`}</p>
|
||||
<div className="text-sm">
|
||||
<div className="mb-6 rounded-md bg-gray-100 p-2">
|
||||
<h1 className="mb-2 ml-1 font-medium text-gray-900">{t("available_variables")}</h1>
|
||||
<div className="mb-2.5 flex font-normal">
|
||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Event type title}`}</p>
|
||||
<p className="text-gray-900">{t("event_name_info")}</p>
|
||||
</div>
|
||||
<div className="mb-2.5 flex font-normal">
|
||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Organiser}`}</p>
|
||||
<p className="text-gray-900">{t("your_full_name")}</p>
|
||||
</div>
|
||||
<div className="mb-2.5 flex font-normal">
|
||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Scheduler}`}</p>
|
||||
<p className="text-gray-900">{t("scheduler_full_name")}</p>
|
||||
</div>
|
||||
<div className="mb-1 flex font-normal">
|
||||
<p className="ml-1 mr-5 w-28 text-gray-400">{`{Location}`}</p>
|
||||
<p className="text-gray-900">{t("location_info")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<h1 className="mb-2 text-[14px] font-medium leading-4">{t("preview")}</h1>
|
||||
<div
|
||||
className="flex h-[212px] w-full rounded-md border-y bg-cover bg-center"
|
||||
style={{
|
||||
backgroundImage: "url(/calendar-preview.svg)",
|
||||
}}>
|
||||
<div className="m-auto flex items-center justify-center self-stretch">
|
||||
<div className="mt-3 ml-11 box-border h-[110px] w-[120px] flex-col items-start gap-1 rounded-md border border-solid border-black bg-gray-100 text-[12px] leading-3">
|
||||
<p className="overflow-hidden text-ellipsis p-1.5 font-medium text-gray-900">
|
||||
{previewText}
|
||||
</p>
|
||||
<p className="ml-1.5 text-[10px] font-normal text-gray-600">8 - 10 AM</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")}>
|
||||
{t("cancel")}
|
||||
</DialogClose>
|
||||
<Button color="primary" onClick={() => setShowEventNameTip(false)}>
|
||||
{t("create")}
|
||||
</Button>
|
||||
<DialogClose onClick={() => formMethods.setValue("eventName", eventType.eventName ?? "")} />
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
@@ -413,7 +479,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<Dialog open={selectedCustomInputModalOpen} onOpenChange={setSelectedCustomInputModalOpen}>
|
||||
<DialogContent
|
||||
type="creation"
|
||||
Icon={Icon.FiPlus}
|
||||
Icon={FiPlus}
|
||||
title={t("add_new_custom_input_field")}
|
||||
description={t("this_input_will_shown_booking_this_event")}>
|
||||
<CustomInputTypeForm
|
||||
|
||||
@@ -7,7 +7,8 @@ import { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
import { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen, Icon } from "@calcom/ui";
|
||||
import { Button, EmptyScreen } from "@calcom/ui";
|
||||
import { FiGrid } from "@calcom/ui/components/icon";
|
||||
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||
EventTypeAppCardComponentProps["eventType"];
|
||||
@@ -60,7 +61,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
<div className="before:border-0">
|
||||
{!isLoading && !installedApps?.length ? (
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiGrid}
|
||||
Icon={FiGrid}
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
|
||||
@@ -10,7 +10,8 @@ import findDurationType from "@calcom/lib/findDurationType";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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";
|
||||
import { Button, DateRangePicker, Input, InputField, Label, Select, SettingsToggle } from "@calcom/ui";
|
||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
@@ -419,19 +420,21 @@ const BookingLimits = () => {
|
||||
defaultValue={BOOKING_LIMIT_OPTIONS.find((option) => option.value === key)}
|
||||
onChange={(val) => {
|
||||
const current = currentBookingLimits;
|
||||
const currentValue = watchBookingLimits[bookingLimitKey];
|
||||
|
||||
// Removes limit from previous selected value (eg when changed from per_week to per_month, we unset per_week here)
|
||||
delete current[bookingLimitKey];
|
||||
const newData = {
|
||||
...current,
|
||||
// Set limit to new selected value (in the example above this means we set the limit to per_week here).
|
||||
[val?.value as BookingLimitsKey]: watchBookingLimits[bookingLimitKey],
|
||||
[val?.value as BookingLimitsKey]: currentValue,
|
||||
};
|
||||
onChange(newData);
|
||||
}}
|
||||
/>
|
||||
<Button
|
||||
variant="icon"
|
||||
StartIcon={Icon.FiTrash}
|
||||
StartIcon={FiTrash}
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
const current = currentBookingLimits;
|
||||
@@ -445,7 +448,7 @@ const BookingLimits = () => {
|
||||
{currentBookingLimits && Object.keys(currentBookingLimits).length <= 3 && (
|
||||
<Button
|
||||
color="minimal"
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => {
|
||||
if (!currentBookingLimits || !watchBookingLimits) return;
|
||||
const currentKeys = Object.keys(watchBookingLimits);
|
||||
@@ -458,7 +461,7 @@ const BookingLimits = () => {
|
||||
|
||||
setValue("bookingLimits", {
|
||||
...watchBookingLimits,
|
||||
[rest[0].value]: undefined,
|
||||
[rest[0].value]: 1,
|
||||
});
|
||||
}}>
|
||||
{t("add_limit")}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
|
||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||
|
||||
import RecurringEventController from "./RecurringEventController";
|
||||
|
||||
export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
||||
const stripeAppData = getStripeAppData(eventType);
|
||||
const paymentAppData = getPaymentAppData(eventType);
|
||||
|
||||
const requirePayment = stripeAppData.price > 0;
|
||||
const requirePayment = paymentAppData.price > 0;
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
|
||||
@@ -9,12 +9,17 @@ import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||
import { MultiValue } from "react-select";
|
||||
import { z } from "zod";
|
||||
|
||||
import { EventLocationType, getEventLocationType, MeetLocationType } from "@calcom/app-store/locations";
|
||||
import {
|
||||
EventLocationType,
|
||||
getEventLocationType,
|
||||
MeetLocationType,
|
||||
LocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
|
||||
import { slugify } from "@lib/slugify";
|
||||
import { slugify } from "@calcom/lib/slugify";
|
||||
import { Button, Label, Select, SettingsToggle, Skeleton, TextField } from "@calcom/ui";
|
||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import LocationSelect, {
|
||||
@@ -34,6 +39,16 @@ const getLocationFromType = (
|
||||
}
|
||||
};
|
||||
|
||||
const getDefaultLocationValue = (options: EventTypeSetupProps["locationOptions"], type: string) => {
|
||||
for (const locationType of options) {
|
||||
for (const location of locationType.options) {
|
||||
if (location.value === type && location.disabled === false) {
|
||||
return location;
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export const EventSetupTab = (
|
||||
props: Pick<EventTypeSetupProps, "eventType" | "locationOptions" | "team" | "teamMembers">
|
||||
) => {
|
||||
@@ -69,7 +84,12 @@ export const EventSetupTab = (
|
||||
const removeLocation = (selectedLocation: typeof eventType.locations[number]) => {
|
||||
formMethods.setValue(
|
||||
"locations",
|
||||
formMethods.getValues("locations").filter((location) => location.type !== selectedLocation.type),
|
||||
formMethods.getValues("locations").filter((location) => {
|
||||
if (location.type === LocationType.InPerson) {
|
||||
return location.address !== selectedLocation.address;
|
||||
}
|
||||
return location.type !== selectedLocation.type;
|
||||
}),
|
||||
{ shouldValidate: true }
|
||||
);
|
||||
};
|
||||
@@ -84,14 +104,14 @@ export const EventSetupTab = (
|
||||
...details,
|
||||
type: newLocationType,
|
||||
};
|
||||
} else {
|
||||
copy[existingIdx] = {
|
||||
...formMethods.getValues("locations")[existingIdx],
|
||||
...details,
|
||||
};
|
||||
}
|
||||
|
||||
formMethods.setValue("locations", copy);
|
||||
formMethods.setValue("locations", [
|
||||
...copy,
|
||||
...(newLocationType === LocationType.InPerson && editingLocationType === ""
|
||||
? [{ ...details, type: newLocationType }]
|
||||
: []),
|
||||
]);
|
||||
} else {
|
||||
formMethods.setValue(
|
||||
"locations",
|
||||
@@ -138,15 +158,19 @@ export const EventSetupTab = (
|
||||
return true;
|
||||
});
|
||||
|
||||
const defaultValue = getDefaultLocationValue(locationOptions, "integrations:daily");
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{validLocations.length === 0 && (
|
||||
<div className="flex">
|
||||
<LocationSelect
|
||||
defaultValue={defaultValue}
|
||||
placeholder={t("select")}
|
||||
options={locationOptions}
|
||||
isSearchable={false}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
menuPlacement="auto"
|
||||
onChange={(e: SingleValueLocationOption) => {
|
||||
if (e?.value) {
|
||||
const newLocationType = e.value;
|
||||
@@ -173,7 +197,9 @@ export const EventSetupTab = (
|
||||
return null;
|
||||
}
|
||||
return (
|
||||
<li key={location.type} className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
<li
|
||||
key={`${location.type}${index}`}
|
||||
className="mb-2 rounded-md border border-gray-300 py-1.5 px-2">
|
||||
<div className="flex max-w-full justify-between">
|
||||
<div key={index} className="flex flex-grow items-center">
|
||||
<img
|
||||
@@ -198,10 +224,10 @@ export const EventSetupTab = (
|
||||
}}
|
||||
aria-label={t("edit")}
|
||||
className="mr-1 p-1 text-gray-500 hover:text-gray-900">
|
||||
<Icon.FiEdit2 className="h-4 w-4" />
|
||||
<FiEdit2 className="h-4 w-4" />
|
||||
</button>
|
||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||
<Icon.FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
||||
<FiX className="border-l-1 h-6 w-6 pl-1 text-gray-500 hover:text-gray-900 " />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -210,7 +236,7 @@ export const EventSetupTab = (
|
||||
})}
|
||||
{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" />
|
||||
<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.
|
||||
@@ -227,7 +253,7 @@ export const EventSetupTab = (
|
||||
)}
|
||||
{validLocations.length > 0 && validLocations.length !== locationOptions.length && (
|
||||
<li>
|
||||
<Button StartIcon={Icon.FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
<Button StartIcon={FiPlus} color="minimal" onClick={() => setShowLocationModal(true)}>
|
||||
{t("add_location")}
|
||||
</Button>
|
||||
</li>
|
||||
|
||||
@@ -25,6 +25,16 @@ const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
||||
email,
|
||||
});
|
||||
|
||||
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||
if (a.label < b.label) {
|
||||
return -1;
|
||||
}
|
||||
if (a.label > b.label) {
|
||||
return 1;
|
||||
}
|
||||
return 0;
|
||||
};
|
||||
|
||||
const FixedHosts = ({
|
||||
control,
|
||||
labelText,
|
||||
@@ -103,7 +113,6 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
];
|
||||
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && (
|
||||
@@ -128,17 +137,16 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
|
||||
{schedulingType === SchedulingType.COLLECTIVE && (
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
placeholder={t("add_attendees")}
|
||||
labelText={t("team")}
|
||||
control={formMethods.control}
|
||||
/>
|
||||
)}
|
||||
|
||||
{schedulingType === SchedulingType.ROUND_ROBIN && (
|
||||
<>
|
||||
<FixedHosts
|
||||
options={teamMembersOptions}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods.getValues("hosts").find((host) => host.userId.toString() === option.value)
|
||||
}
|
||||
@@ -173,7 +181,7 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
)
|
||||
.filter(Boolean)}
|
||||
controlShouldRenderValue={false}
|
||||
options={teamMembers.map(mapUserToValue)}
|
||||
options={teamMembersOptions.sort(sortByLabel)}
|
||||
isOptionDisabled={(option) =>
|
||||
!!formMethods
|
||||
.getValues("hostsFixed")
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||
import { useState } from "react";
|
||||
import { TbWebhook } from "react-icons/tb";
|
||||
|
||||
@@ -9,7 +9,8 @@ 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";
|
||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
export const EventTeamWebhooksTab = ({
|
||||
eventType,
|
||||
@@ -84,7 +85,7 @@ export const EventTeamWebhooksTab = ({
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={Icon.FiPlus}
|
||||
StartIcon={FiPlus}
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
@@ -99,7 +100,7 @@ export const EventTeamWebhooksTab = ({
|
||||
<>
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="mt-4 mb-8 rounded-md border">
|
||||
<div className="mb-8 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
@@ -130,7 +131,10 @@ export const EventTeamWebhooksTab = ({
|
||||
|
||||
{/* New webhook dialog */}
|
||||
<Dialog open={createModalOpen} onOpenChange={(isOpen) => !isOpen && setCreateModalOpen(false)}>
|
||||
<DialogContent title={t("create_webhook")} description={t("create_webhook_team_event_type")}>
|
||||
<DialogContent
|
||||
enableOverflow
|
||||
title={t("create_webhook")}
|
||||
description={t("create_webhook_team_event_type")}>
|
||||
<WebhookForm
|
||||
onSubmit={onCreateWebhook}
|
||||
onCancel={() => setCreateModalOpen(false)}
|
||||
|
||||
@@ -23,7 +23,6 @@ import {
|
||||
DropdownItem,
|
||||
DropdownMenuTrigger,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
Label,
|
||||
showToast,
|
||||
Skeleton,
|
||||
@@ -32,6 +31,21 @@ import {
|
||||
VerticalDivider,
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiLink,
|
||||
FiCalendar,
|
||||
FiClock,
|
||||
FiSliders,
|
||||
FiRepeat,
|
||||
FiGrid,
|
||||
FiZap,
|
||||
FiUsers,
|
||||
FiExternalLink,
|
||||
FiCode,
|
||||
FiTrash,
|
||||
FiMoreHorizontal,
|
||||
FiLoader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
|
||||
@@ -63,44 +77,44 @@ function getNavigation(props: {
|
||||
{
|
||||
name: "event_setup_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||
icon: Icon.FiLink,
|
||||
icon: FiLink,
|
||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "availability",
|
||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||
icon: Icon.FiCalendar,
|
||||
icon: FiCalendar,
|
||||
info: `default_schedule_name`, // TODO: Get this from props
|
||||
},
|
||||
{
|
||||
name: "event_limit_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||
icon: Icon.FiClock,
|
||||
icon: FiClock,
|
||||
info: `event_limit_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "event_advanced_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||
icon: Icon.FiSliders,
|
||||
icon: FiSliders,
|
||||
info: `event_advanced_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||
icon: Icon.FiRepeat,
|
||||
icon: FiRepeat,
|
||||
info: `recurring_event_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||
icon: Icon.FiGrid,
|
||||
icon: FiGrid,
|
||||
//TODO: Handle proper translation with count handling
|
||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||
icon: Icon.FiZap,
|
||||
icon: FiZap,
|
||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||
},
|
||||
];
|
||||
@@ -157,7 +171,7 @@ function EventTypeSingleLayout({
|
||||
navigation.splice(2, 0, {
|
||||
name: "assignment",
|
||||
href: `/event-types/${eventType.id}?tabName=team`,
|
||||
icon: Icon.FiUsers,
|
||||
icon: FiUsers,
|
||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||
});
|
||||
navigation.push({
|
||||
@@ -181,7 +195,6 @@ function EventTypeSingleLayout({
|
||||
backPath="/event-types"
|
||||
title={eventType.title + " | " + t("event_type")}
|
||||
heading={eventType.title}
|
||||
subtitle={eventType.description || ""}
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
|
||||
@@ -211,14 +224,14 @@ function EventTypeSingleLayout({
|
||||
variant="icon"
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
StartIcon={FiExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={Icon.FiLink}
|
||||
StartIcon={FiLink}
|
||||
tooltip={t("copy_link")}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
@@ -227,7 +240,7 @@ function EventTypeSingleLayout({
|
||||
/>
|
||||
<EmbedButton
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
StartIcon={Icon.FiCode}
|
||||
StartIcon={FiCode}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
tooltip={t("embed")}
|
||||
@@ -235,7 +248,7 @@ function EventTypeSingleLayout({
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={Icon.FiTrash}
|
||||
StartIcon={FiTrash}
|
||||
tooltip={t("delete")}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
@@ -246,19 +259,14 @@ function EventTypeSingleLayout({
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button
|
||||
className="lg:hidden"
|
||||
StartIcon={Icon.FiMoreHorizontal}
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
/>
|
||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<DropdownItem
|
||||
target="_blank"
|
||||
type="button"
|
||||
StartIcon={Icon.FiExternalLink}
|
||||
StartIcon={FiExternalLink}
|
||||
href={permalink}
|
||||
rel="noreferrer">
|
||||
{t("preview")}
|
||||
@@ -267,7 +275,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={Icon.FiLink}
|
||||
StartIcon={FiLink}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
@@ -278,7 +286,8 @@ function EventTypeSingleLayout({
|
||||
<DropdownMenuItem className="focus:ring-gray-100">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={Icon.FiTrash}
|
||||
color="destructive"
|
||||
StartIcon={FiTrash}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
{t("delete")}
|
||||
@@ -313,8 +322,8 @@ function EventTypeSingleLayout({
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Suspense fallback={<Icon.FiLoader />}>
|
||||
<div className="-mt-2 flex flex-col xl:flex-row xl:space-x-8">
|
||||
<Suspense fallback={<FiLoader />}>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
className="primary-navigation"
|
||||
@@ -330,7 +339,7 @@ function EventTypeSingleLayout({
|
||||
<div
|
||||
className={classNames(
|
||||
"mt-4 rounded-md border-gray-200 bg-white sm:mx-0 xl:mt-0",
|
||||
disableBorder ? "border-0 xl:-mt-4 " : "p-2 md:border md:p-6"
|
||||
disableBorder ? "border-0 " : "p-2 md:border md:p-6"
|
||||
)}>
|
||||
{children}
|
||||
</div>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import { Icon, SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
||||
|
||||
function SkeletonLoader() {
|
||||
return (
|
||||
@@ -31,11 +32,11 @@ function SkeletonItem() {
|
||||
<div className="">
|
||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Icon.FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<FiClock className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<Icon.FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<FiUser className="mt-0.5 mr-1.5 inline h-4 w-4 text-gray-200" />
|
||||
<SkeletonText className="h-4 w-12" />
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { CalendarSwitch } from "./CalendarSwitch";
|
||||
|
||||
interface IConnectedCalendarItem {
|
||||
@@ -21,7 +19,7 @@ interface IConnectedCalendarItem {
|
||||
|
||||
const ConnectedCalendarItem = (prop: IConnectedCalendarItem) => {
|
||||
const { name, logo, externalId, calendars, integrationType } = prop;
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="flex flex-row items-center p-4">
|
||||
|
||||
@@ -1,15 +1,19 @@
|
||||
import { ArrowRightIcon } from "@heroicons/react/solid";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { useRouter } from "next/router";
|
||||
import { FormEvent, useRef, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import turndown from "@calcom/lib/turndownService";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, ImageUploader, showToast, TextArea } from "@calcom/ui";
|
||||
import { Button, Editor, ImageUploader, Label, showToast } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
|
||||
import type { IOnboardingPageProps } from "../../../pages/getting-started/[[...step]]";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||
|
||||
type FormData = {
|
||||
bio: string;
|
||||
};
|
||||
@@ -21,13 +25,10 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
const { user } = props;
|
||||
const { t } = useLocale();
|
||||
const avatarRef = useRef<HTMLInputElement>(null!);
|
||||
const bioRef = useRef<HTMLTextAreaElement>(null);
|
||||
const {
|
||||
register,
|
||||
setValue,
|
||||
handleSubmit,
|
||||
formState: { errors },
|
||||
} = useForm<FormData>({ defaultValues: { bio: user?.bio || "" } });
|
||||
const { setValue, handleSubmit, getValues } = useForm<FormData>({
|
||||
defaultValues: { bio: user?.bio || "" },
|
||||
});
|
||||
|
||||
const { data: eventTypes } = trpc.viewer.eventTypes.list.useQuery();
|
||||
const [imageSrc, setImageSrc] = useState<string>(user?.avatar || "");
|
||||
const utils = trpc.useContext();
|
||||
@@ -138,25 +139,12 @@ const UserProfile = (props: IUserProfileProps) => {
|
||||
</div>
|
||||
</div>
|
||||
<fieldset className="mt-8">
|
||||
<label htmlFor="bio" className="mb-2 block text-sm font-medium text-gray-700">
|
||||
{t("about")}
|
||||
</label>
|
||||
<TextArea
|
||||
{...register("bio", { required: true })}
|
||||
ref={bioRef}
|
||||
name="bio"
|
||||
id="bio"
|
||||
className="mt-1 block h-[60px] w-full rounded-sm border border-gray-300 px-3 py-2 focus:border-gray-500 focus:outline-none focus:ring-gray-500 sm:text-sm"
|
||||
defaultValue={user?.bio || undefined}
|
||||
onChange={(event) => {
|
||||
setValue("bio", event.target.value);
|
||||
}}
|
||||
<Label className="mb-2 block text-sm font-medium text-gray-700">{t("about")}</Label>
|
||||
<Editor
|
||||
getText={() => md.render(getValues("bio") || user?.bio || "")}
|
||||
setText={(value: string) => setValue("bio", turndown(value))}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
/>
|
||||
{errors.bio && (
|
||||
<p data-testid="required" className="py-2 text-xs text-red-500">
|
||||
{t("required")}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-2 font-sans text-sm font-normal text-gray-600 dark:text-white">
|
||||
{t("few_sentences_about_yourself")}
|
||||
</p>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { ShieldCheckIcon } from "@calcom/ui/components/icon";
|
||||
|
||||
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">
|
||||
<Icon.ShieldCheckIcon className="h-6 w-6 text-white" />
|
||||
<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">
|
||||
|
||||
@@ -2,7 +2,8 @@ import { useMutation } from "@tanstack/react-query";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Badge, Icon, showToast, Switch } from "@calcom/ui";
|
||||
import { Badge, showToast, Switch } from "@calcom/ui";
|
||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
||||
|
||||
export function CalendarSwitch(props: {
|
||||
type: string;
|
||||
@@ -74,7 +75,7 @@ export function CalendarSwitch(props: {
|
||||
/>
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<Icon.FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
<FiArrowLeft className="mr-1" /> {t("adding_events_to")}
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
|
||||
+30
-10
@@ -1,17 +1,39 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import classNames from "classnames";
|
||||
import { signIn } from "next-auth/react";
|
||||
import { useRouter } from "next/router";
|
||||
import React from "react";
|
||||
import { Controller, FormProvider, useForm } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmailField, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||
import { FiUserCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
const router = useRouter();
|
||||
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||
const { t } = useLocale();
|
||||
if (props.userCount > 0)
|
||||
return (
|
||||
<form
|
||||
id="wizard-step-1"
|
||||
name="wizard-step-1"
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
props.onSuccess();
|
||||
}}>
|
||||
<EmptyScreen
|
||||
Icon={FiUserCheck}
|
||||
headline={t("admin_user_created")}
|
||||
description={t("admin_user_created_description")}
|
||||
/>
|
||||
</form>
|
||||
);
|
||||
return <AdminUser {...props} />;
|
||||
};
|
||||
|
||||
export const AdminUser = (props: { onSubmit: () => void; onError: () => void; onSuccess: () => void }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const formSchema = z.object({
|
||||
@@ -45,11 +67,11 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
});
|
||||
|
||||
const onError = () => {
|
||||
props.setIsLoading(false);
|
||||
props.onError();
|
||||
};
|
||||
|
||||
const onSubmit = formMethods.handleSubmit(async (data: z.infer<typeof formSchema>) => {
|
||||
props.setIsLoading(true);
|
||||
props.onSubmit();
|
||||
const response = await fetch("/api/auth/setup", {
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
@@ -69,9 +91,9 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
email: data.email_address.toLowerCase(),
|
||||
password: data.password,
|
||||
});
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
props.onSuccess();
|
||||
} else {
|
||||
router.replace("/auth/setup");
|
||||
props.onError();
|
||||
}
|
||||
}, onError);
|
||||
|
||||
@@ -186,5 +208,3 @@ const SetupFormStep1 = (props: { setIsLoading: (val: boolean) => void }) => {
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default SetupFormStep1;
|
||||
@@ -0,0 +1,76 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import classNames from "classnames";
|
||||
import { useState } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
const ENTERPRISE_BOOKING_FEE = "$99"; // TODO: get this from a new API endpoint
|
||||
|
||||
const ChooseLicense = (
|
||||
props: {
|
||||
value: string;
|
||||
onChange: (value: string) => void;
|
||||
onSubmit: (value: string) => void;
|
||||
} & Omit<JSX.IntrinsicElements["form"], "onSubmit" | "onChange">
|
||||
) => {
|
||||
const { value: initialValue = "FREE", onChange, onSubmit, ...rest } = props;
|
||||
const [value, setValue] = useState(initialValue);
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<form
|
||||
{...rest}
|
||||
className="space-y-4"
|
||||
onSubmit={(e) => {
|
||||
e.preventDefault();
|
||||
onSubmit(value);
|
||||
}}>
|
||||
<RadioGroup.Root
|
||||
defaultValue={initialValue}
|
||||
value={value}
|
||||
aria-label={t("choose_a_license")}
|
||||
className="grid grid-rows-2 gap-4 md:grid-cols-2 md:grid-rows-1"
|
||||
onValueChange={(value) => {
|
||||
onChange(value);
|
||||
setValue(value);
|
||||
}}>
|
||||
<RadioGroup.Item value="FREE">
|
||||
<div
|
||||
className={classNames(
|
||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||
value === "FREE" && "ring-2 ring-black"
|
||||
)}>
|
||||
<h2 className="font-cal text-xl text-black">{t("agplv3_license")}</h2>
|
||||
<p className="font-medium text-green-800">{t("free_license_fee")}</p>
|
||||
<p className="text-gray-500">{t("forever_open_and_free")}</p>
|
||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||
<li>{t("required_to_keep_your_code_open_source")}</li>
|
||||
<li>{t("cannot_repackage_and_resell")}</li>
|
||||
<li>{t("no_enterprise_features")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</RadioGroup.Item>
|
||||
<RadioGroup.Item value="EE">
|
||||
<div
|
||||
className={classNames(
|
||||
"cursor-pointer space-y-2 rounded-md border bg-white p-4 hover:border-black",
|
||||
value === "EE" && "ring-2 ring-black"
|
||||
)}>
|
||||
<h2 className="font-cal text-xl text-black">{t("ee_enterprise_license")}</h2>
|
||||
<p className="font-medium text-green-800">
|
||||
{t("enterprise_booking_fee", { enterprise_booking_fee: ENTERPRISE_BOOKING_FEE })}
|
||||
</p>
|
||||
<p className="text-gray-500">{t("enterprise_license_includes")}</p>
|
||||
<ul className="ml-4 list-disc text-left text-xs text-gray-500">
|
||||
<li>{t("no_need_to_keep_your_code_open_source")}</li>
|
||||
<li>{t("repackage_rebrand_resell")}</li>
|
||||
<li>{t("a_vast_suite_of_enterprise_features")}</li>
|
||||
</ul>
|
||||
</div>
|
||||
</RadioGroup.Item>
|
||||
</RadioGroup.Root>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
|
||||
export default ChooseLicense;
|
||||
@@ -0,0 +1,144 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { noop } from "lodash";
|
||||
import { useCallback, useState } from "react";
|
||||
import { Controller, FormProvider, useForm, useFormState } from "react-hook-form";
|
||||
import * as z from "zod";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { CONSOLE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterInputs, RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Button, TextField } from "@calcom/ui";
|
||||
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
||||
|
||||
type EnterpriseLicenseFormValues = {
|
||||
licenseKey: string;
|
||||
};
|
||||
|
||||
const makeSchemaLicenseKey = (args: { callback: (valid: boolean) => void; onSuccessValidate: () => void }) =>
|
||||
z.object({
|
||||
licenseKey: z
|
||||
.string()
|
||||
.uuid({
|
||||
message: "License key must follow UUID format: 8-4-4-4-12",
|
||||
})
|
||||
.superRefine(async (data, ctx) => {
|
||||
const parse = z.string().uuid().safeParse(data);
|
||||
if (parse.success) {
|
||||
args.callback(true);
|
||||
const response = await fetch(`${CONSOLE_URL}/api/license?key=${data}`);
|
||||
args.callback(false);
|
||||
const json = await response.json();
|
||||
if (!json.valid) {
|
||||
ctx.addIssue({
|
||||
code: z.ZodIssueCode.custom,
|
||||
message: `License key ${json.message.toLowerCase()}`,
|
||||
});
|
||||
} else {
|
||||
args.onSuccessValidate();
|
||||
}
|
||||
}
|
||||
}),
|
||||
});
|
||||
|
||||
const EnterpriseLicense = (
|
||||
props: {
|
||||
licenseKey?: string;
|
||||
initialValue?: Partial<EnterpriseLicenseFormValues>;
|
||||
onSuccessValidate: () => void;
|
||||
onSubmit: (value: EnterpriseLicenseFormValues) => void;
|
||||
onSuccess?: (
|
||||
data: RouterOutputs["viewer"]["deploymentSetup"]["update"],
|
||||
variables: RouterInputs["viewer"]["deploymentSetup"]["update"]
|
||||
) => void;
|
||||
} & Omit<JSX.IntrinsicElements["form"], "onSubmit">
|
||||
) => {
|
||||
const { onSubmit, onSuccess = noop, onSuccessValidate = noop, ...rest } = props;
|
||||
const { t } = useLocale();
|
||||
const [checkLicenseLoading, setCheckLicenseLoading] = useState(false);
|
||||
const mutation = trpc.viewer.deploymentSetup.update.useMutation({
|
||||
onSuccess,
|
||||
});
|
||||
|
||||
const schemaLicenseKey = useCallback(
|
||||
() =>
|
||||
makeSchemaLicenseKey({
|
||||
callback: setCheckLicenseLoading,
|
||||
onSuccessValidate,
|
||||
}),
|
||||
[setCheckLicenseLoading, onSuccessValidate]
|
||||
);
|
||||
|
||||
const formMethods = useForm<EnterpriseLicenseFormValues>({
|
||||
defaultValues: {
|
||||
licenseKey: props.licenseKey || "",
|
||||
},
|
||||
resolver: zodResolver(schemaLicenseKey()),
|
||||
});
|
||||
|
||||
const handleSubmit = formMethods.handleSubmit((values) => {
|
||||
onSubmit(values);
|
||||
setCheckLicenseLoading(false);
|
||||
mutation.mutate(values);
|
||||
});
|
||||
|
||||
const { isDirty, errors } = useFormState(formMethods);
|
||||
|
||||
return (
|
||||
<FormProvider {...formMethods}>
|
||||
<form {...rest} className="space-y-4 rounded-md bg-white px-8 py-10" onSubmit={handleSubmit}>
|
||||
<div>
|
||||
<Button
|
||||
className="w-full justify-center text-lg"
|
||||
EndIcon={FiExternalLink}
|
||||
href="https://console.cal.com"
|
||||
target="_blank">
|
||||
{t("purchase_license")}
|
||||
</Button>
|
||||
<div className="relative flex justify-center">
|
||||
<hr className="my-8 w-full border-[1.5px] border-gray-200" />
|
||||
<span className="absolute mt-[22px] bg-white px-3.5 text-sm">OR</span>
|
||||
</div>
|
||||
{t("already_have_key")}
|
||||
<Controller
|
||||
name="licenseKey"
|
||||
control={formMethods.control}
|
||||
render={({ field: { onBlur, onChange, value } }) => (
|
||||
<TextField
|
||||
{...formMethods.register("licenseKey")}
|
||||
className={classNames(
|
||||
"mb-0 group-hover:border-gray-400",
|
||||
(checkLicenseLoading || (errors.licenseKey === undefined && isDirty)) && "border-r-0"
|
||||
)}
|
||||
placeholder="xxxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxxx"
|
||||
labelSrOnly={true}
|
||||
value={value}
|
||||
addOnFilled={false}
|
||||
addOnClassname={classNames(
|
||||
"hover:border-gray-300",
|
||||
errors.licenseKey === undefined && isDirty && "group-hover:border-gray-400"
|
||||
)}
|
||||
addOnSuffix={
|
||||
checkLicenseLoading ? (
|
||||
<FiLoader className="h-5 w-5 animate-spin" />
|
||||
) : errors.licenseKey === undefined && isDirty ? (
|
||||
<FiCheck className="h-5 w-5 text-green-700" />
|
||||
) : undefined
|
||||
}
|
||||
color={errors.licenseKey ? "warn" : ""}
|
||||
onBlur={onBlur}
|
||||
onChange={async (e: React.ChangeEvent<HTMLInputElement>) => {
|
||||
onChange(e.target.value);
|
||||
formMethods.setValue("licenseKey", e.target.value);
|
||||
await formMethods.trigger("licenseKey");
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
</form>
|
||||
</FormProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default EnterpriseLicense;
|
||||
+13
-7
@@ -1,24 +1,30 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
const StepDone = () => {
|
||||
const StepDone = (props: {
|
||||
currentStep: number;
|
||||
nextStepPath: string;
|
||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<form
|
||||
id="wizard-step-1"
|
||||
name="wizard-step-1"
|
||||
className="space-y-4"
|
||||
id={`wizard-step-${props.currentStep}`}
|
||||
name={`wizard-step-${props.currentStep}`}
|
||||
className="flex justify-center space-y-4"
|
||||
onSubmit={(e) => {
|
||||
props.setIsLoading(true);
|
||||
e.preventDefault();
|
||||
router.replace(`/auth/setup?step=2&category=calendar`);
|
||||
router.replace(props.nextStepPath);
|
||||
}}>
|
||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600 dark:bg-white">
|
||||
<Icon.FiCheck className="inline-block h-10 w-10 text-white dark:bg-white dark:text-gray-600" />
|
||||
<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>
|
||||
@@ -1,3 +1,4 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import Link from "next/link";
|
||||
import { TeamPageProps } from "pages/team/[slug]";
|
||||
|
||||
@@ -6,6 +7,8 @@ import { Avatar } from "@calcom/ui";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
type TeamType = TeamPageProps["team"];
|
||||
type MembersType = TeamType["members"];
|
||||
type MemberType = MembersType[number];
|
||||
@@ -13,9 +16,11 @@ type MemberType = MembersType[number];
|
||||
const Member = ({ member, teamName }: { member: MemberType; teamName: string | null }) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
const isBioEmpty = !member.bio || !member.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
return (
|
||||
<Link key={member.id} href={`/${member.username}`}>
|
||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full w-[90%] flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
||||
<div className="sm:min-w-80 sm:max-w-80 dark:bg-darkgray-200 dark:hover:bg-darkgray-300 group flex min-h-full flex-col space-y-2 rounded-md bg-white p-4 hover:cursor-pointer hover:bg-gray-50 ">
|
||||
<Avatar
|
||||
size="md"
|
||||
alt={member.name || ""}
|
||||
@@ -23,9 +28,18 @@ const Member = ({ member, teamName }: { member: MemberType; teamName: string | n
|
||||
/>
|
||||
<section className="line-clamp-4 mt-2 w-full space-y-1">
|
||||
<p className="font-medium text-gray-900 dark:text-white">{member.name}</p>
|
||||
<p className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
|
||||
{member.bio || t("user_from_team", { user: member.name, team: teamName })}
|
||||
</p>
|
||||
<div className="line-clamp-3 overflow-ellipsis text-sm font-normal text-gray-500 dark:text-white">
|
||||
{!isBioEmpty ? (
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(member.bio || "") }}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
t("user_from_team", { user: member.name, team: teamName })
|
||||
)}
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</Link>
|
||||
@@ -46,7 +60,7 @@ const Members = ({ members, teamName }: { members: MembersType; teamName: string
|
||||
);
|
||||
};
|
||||
|
||||
const Team = ({ team }: TeamPageProps) => {
|
||||
const Team = ({ team }: Omit<TeamPageProps, "trpcState">) => {
|
||||
return (
|
||||
<div>
|
||||
<Members members={team.members} teamName={team.name} />
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import classNames from "classnames";
|
||||
|
||||
import { APP_NAME, LOGO } from "@calcom/lib/constants";
|
||||
import { Credits, HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
|
||||
import Loader from "@components/Loader";
|
||||
|
||||
@@ -35,10 +35,7 @@ export default function AuthContainer(props: React.PropsWithChildren<Props>) {
|
||||
<div className="mx-2 rounded-md border border-gray-200 bg-white px-4 py-10 sm:px-10">
|
||||
{props.children}
|
||||
</div>
|
||||
<div className="mt-8 text-center text-sm text-gray-600">
|
||||
{props.footerText}
|
||||
<Credits />
|
||||
</div>
|
||||
<div className="mt-8 text-center text-sm text-gray-600">{props.footerText}</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import classNames from "classnames";
|
||||
import React, { useState } from "react";
|
||||
import { useState } from "react";
|
||||
import { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
const EditableHeading = function EditableHeading({
|
||||
value,
|
||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
||||
<label className="min-w-8 relative inline-block">
|
||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||
{!isEditing && isReady && (
|
||||
<Icon.FiEdit2 className="ml-1 inline h-4 w-4 align-top text-gray-700 group-hover:text-gray-500" />
|
||||
<FiEdit2 className=" ml-1 -mt-px inline h-3 w-3 text-gray-500 group-hover:text-gray-500" />
|
||||
)}
|
||||
<input
|
||||
{...passThroughProps}
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
import { Icon, Tooltip } from "@calcom/ui";
|
||||
import { Tooltip } from "@calcom/ui";
|
||||
import { FiInfo } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function InfoBadge({ content }: { content: string }) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip side="top" content={content}>
|
||||
<span title={content}>
|
||||
<Icon.FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
|
||||
<FiInfo className="relative top-px left-1 right-1 mt-px h-4 w-4 text-gray-500" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -11,7 +11,8 @@ 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 } from "@calcom/ui";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, Input, Label } from "@calcom/ui";
|
||||
import { FiCheck, FiEdit2, FiExternalLink, StarIconSolid } from "@calcom/ui/components/icon";
|
||||
|
||||
export enum UsernameChangeStatusEnum {
|
||||
UPGRADE = "UPGRADE",
|
||||
@@ -230,8 +231,8 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
isInputUsernamePremium ? "text-orange-400" : "",
|
||||
usernameIsAvailable ? "" : ""
|
||||
)}>
|
||||
{isInputUsernamePremium ? <Icon.StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <Icon.FiCheck className="mt-2 w-6" /> : <></>}
|
||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -249,7 +250,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row">
|
||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiEdit2 className="m-auto h-6 w-6" />
|
||||
<FiEdit2 className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="mb-4 w-full px-4 pt-1">
|
||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||
@@ -283,7 +284,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
data-testid="go-to-billing"
|
||||
href={paymentLink}>
|
||||
<>
|
||||
{t("go_to_stripe_billing")} <Icon.FiExternalLink className="ml-1 h-4 w-4" />
|
||||
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
|
||||
</>
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -7,17 +7,8 @@ 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,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { Button, Dialog, DialogClose, DialogContent, DialogHeader, TextField } from "@calcom/ui";
|
||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
interface ICustomUsernameProps {
|
||||
currentUsername: string | undefined;
|
||||
@@ -145,7 +136,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
{currentUsername !== inputUsernameValue && (
|
||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||
<span className={classNames("mx-2 py-2")}>
|
||||
{usernameIsAvailable ? <Icon.FiCheck className="w-6" /> : <></>}
|
||||
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -165,7 +156,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
<DialogContent>
|
||||
<div style={{ display: "flex", flexDirection: "row" }}>
|
||||
<div className="xs:hidden flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<Icon.FiEdit2 className="m-auto h-6 w-6" />
|
||||
<FiEdit2 className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="mb-4 w-full px-4 pt-1">
|
||||
<DialogHeader title={t("confirm_username_change_dialog_title")} />
|
||||
|
||||
@@ -2,7 +2,8 @@ import React from "react";
|
||||
import { Props } from "react-select";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, Avatar } from "@calcom/ui";
|
||||
import { Avatar } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import Select from "@components/ui/form/Select";
|
||||
|
||||
@@ -48,7 +49,7 @@ export const CheckedSelect = ({
|
||||
alt={option.label}
|
||||
/>
|
||||
{option.label}
|
||||
<Icon.FiX
|
||||
<FiX
|
||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||
className="float-right mt-0.5 h-5 w-5 cursor-pointer text-gray-500"
|
||||
/>
|
||||
|
||||
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
|
||||
import "react-date-picker/dist/DatePicker.css";
|
||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { FiCalendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import classNames from "@lib/classNames";
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
calendarIcon={<Icon.FiCalendar className="h-5 w-5 text-gray-500" />}
|
||||
calendarIcon={<FiCalendar className="h-5 w-5 text-gray-500" />}
|
||||
value={date}
|
||||
minDate={minDate}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -13,18 +13,20 @@ import { trpc } from "@calcom/trpc/react";
|
||||
import { MetaProvider } from "@calcom/ui";
|
||||
|
||||
import usePublicPage from "@lib/hooks/usePublicPage";
|
||||
import { WithNonceProps } from "@lib/withNonce";
|
||||
|
||||
const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children: React.ReactNode }>(
|
||||
({ children }) => <>{children}</>
|
||||
);
|
||||
|
||||
// Workaround for https://github.com/vercel/next.js/issues/8592
|
||||
export type AppProps = Omit<NextAppProps, "Component"> & {
|
||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
||||
Component: NextAppProps["Component"] & {
|
||||
requiresLicense?: boolean;
|
||||
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
||||
};
|
||||
|
||||
/** Will be defined only is there was an error */
|
||||
err?: Error;
|
||||
};
|
||||
@@ -77,6 +79,7 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
<TooltipProvider>
|
||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
||||
<ThemeProvider
|
||||
nonce={props.pageProps.nonce}
|
||||
enableColorScheme={false}
|
||||
storageKey={storageKey}
|
||||
forcedTheme={forcedTheme}
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@calcom/lib/availability";
|
||||
@@ -1,2 +0,0 @@
|
||||
// TODO: Remove this file once everything is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/constants";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@calcom/lib/weekday";
|
||||
@@ -0,0 +1,83 @@
|
||||
import crypto from "crypto";
|
||||
import { IncomingMessage, OutgoingMessage } from "http";
|
||||
import { z } from "zod";
|
||||
|
||||
import { IS_PRODUCTION } from "@calcom/lib/constants";
|
||||
|
||||
function getCspPolicy(nonce: string) {
|
||||
//TODO: Do we need to explicitly define it in turbo.json
|
||||
const CSP_POLICY = process.env.CSP_POLICY;
|
||||
|
||||
// Note: "non-strict" policy only allows inline styles otherwise it's the same as "strict"
|
||||
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||
|
||||
return `
|
||||
default-src 'self' ${IS_PRODUCTION ? "" : "data:"};
|
||||
script-src ${
|
||||
IS_PRODUCTION
|
||||
? // 'self' 'unsafe-inline' https: added for Browsers not supporting strict-dynamic not supporting strict-dynamic
|
||||
"'nonce-" + nonce + "' 'strict-dynamic' 'self' 'unsafe-inline' https:"
|
||||
: // Note: We could use 'strict-dynamic' with 'nonce-..' instead of unsafe-inline but there are some streaming related scripts that get blocked(because they don't have nonce on them). It causes a really frustrating full page error model by Next.js to show up sometimes
|
||||
"'unsafe-inline' 'unsafe-eval' https: http:"
|
||||
};
|
||||
object-src 'none';
|
||||
base-uri 'none';
|
||||
child-src app.cal.com;
|
||||
style-src 'self' ${
|
||||
IS_PRODUCTION ? (useNonStrictPolicy ? "'unsafe-inline'" : "") : "'unsafe-inline'"
|
||||
} app.cal.com;
|
||||
font-src 'self';
|
||||
img-src 'self' https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:
|
||||
`;
|
||||
}
|
||||
|
||||
// Taken from @next-safe/middleware
|
||||
const isPagePathRequest = (url: URL) => {
|
||||
const isNonPagePathPrefix = /^\/(?:_next|api)\//;
|
||||
const isFile = /\..*$/;
|
||||
const { pathname } = url;
|
||||
return !isNonPagePathPrefix.test(pathname) && !isFile.test(pathname);
|
||||
};
|
||||
|
||||
export function csp(req: IncomingMessage | null, res: OutgoingMessage | null) {
|
||||
if (!req) {
|
||||
return { nonce: undefined };
|
||||
}
|
||||
const existingNonce = req.headers["x-nonce"];
|
||||
if (existingNonce) {
|
||||
const existingNoneParsed = z.string().safeParse(existingNonce);
|
||||
return { nonce: existingNoneParsed.success ? existingNoneParsed.data : "" };
|
||||
}
|
||||
if (!req.url) {
|
||||
return { nonce: undefined };
|
||||
}
|
||||
const CSP_POLICY = process.env.CSP_POLICY;
|
||||
const cspEnabledForInstance = CSP_POLICY;
|
||||
const nonce = crypto.randomBytes(16).toString("base64");
|
||||
|
||||
const parsedUrl = new URL(req.url, "http://base_url");
|
||||
const cspEnabledForPage = cspEnabledForInstance && isPagePathRequest(parsedUrl);
|
||||
if (!cspEnabledForPage) {
|
||||
return {
|
||||
nonce: undefined,
|
||||
};
|
||||
}
|
||||
// Set x-nonce request header to be used by `getServerSideProps` or similar fns and `Document.getInitialProps` to read the nonce from
|
||||
// It is generated for all page requests but only used by pages that need CSP
|
||||
req.headers["x-nonce"] = nonce;
|
||||
|
||||
if (res) {
|
||||
res.setHeader(
|
||||
req.headers["x-csp-enforce"] === "true"
|
||||
? "Content-Security-Policy"
|
||||
: "Content-Security-Policy-Report-Only",
|
||||
getCspPolicy(nonce)
|
||||
.replace(/\s{2,}/g, " ")
|
||||
.trim()
|
||||
);
|
||||
}
|
||||
|
||||
return { nonce };
|
||||
}
|
||||
@@ -1,3 +0,0 @@
|
||||
/* Prefer import from `@calcom/lib/isOutOfBounds` */
|
||||
export * from "@calcom/lib/isOutOfBounds";
|
||||
export { default } from "@calcom/lib/isOutOfBounds";
|
||||
@@ -1 +0,0 @@
|
||||
export * from "@calcom/core/location";
|
||||
@@ -1 +0,0 @@
|
||||
export { default } from "@calcom/prisma";
|
||||
@@ -1,2 +0,0 @@
|
||||
// TODO: Remove this file once everything is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/random";
|
||||
@@ -1,3 +0,0 @@
|
||||
/** Prefer import from `@calcom/lib/slots` */
|
||||
export * from "@calcom/lib/slots";
|
||||
export { default } from "@calcom/lib/slots";
|
||||
@@ -1,3 +0,0 @@
|
||||
// TODO: Remove this file once every `classNames` is imported from `@calcom/lib`
|
||||
export * from "@calcom/lib/slugify";
|
||||
export { default } from "@calcom/lib/slugify";
|
||||
@@ -0,0 +1,41 @@
|
||||
import { GetServerSideProps, GetServerSidePropsContext } from "next";
|
||||
|
||||
import { csp } from "@lib/csp";
|
||||
|
||||
export type WithNonceProps = {
|
||||
nonce?: string;
|
||||
};
|
||||
|
||||
/**
|
||||
* Make any getServerSideProps fn return the nonce so that it can be used by Components in the page to add any script tag.
|
||||
* Note that if the Components are not adding any script tag then this is not needed. Even in absence of this, Document.getInitialProps would be able to generate nonce itself which it needs to add script tags common to all pages
|
||||
* There is no harm in wrapping a `getServerSideProps` fn with this even if it doesn't add any script tag.
|
||||
*/
|
||||
export default function withNonce(getServerSideProps: GetServerSideProps) {
|
||||
return async (context: GetServerSidePropsContext) => {
|
||||
const ssrResponse = await getServerSideProps(context);
|
||||
const { nonce } = csp(context.req, context.res);
|
||||
|
||||
// Skip nonce property if it's not available instead of setting it to undefined because undefined can't be serialized.
|
||||
const nonceProps = nonce
|
||||
? {
|
||||
nonce,
|
||||
}
|
||||
: null;
|
||||
|
||||
if (!("props" in ssrResponse)) {
|
||||
return ssrResponse;
|
||||
}
|
||||
|
||||
// Helps in debugging that withNonce was used but a valid nonce couldn't be set
|
||||
context.res.setHeader("x-csp", nonce ? "ssr" : "false");
|
||||
|
||||
return {
|
||||
...ssrResponse,
|
||||
props: {
|
||||
...ssrResponse.props,
|
||||
...nonceProps,
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
+41
-1
@@ -1,3 +1,4 @@
|
||||
import { get } from "@vercel/edge-config";
|
||||
import { collectEvents } from "next-collect/server";
|
||||
import { NextMiddleware, NextResponse, userAgent } from "next/server";
|
||||
|
||||
@@ -8,6 +9,28 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
||||
const middleware: NextMiddleware = async (req) => {
|
||||
const url = req.nextUrl;
|
||||
|
||||
if (!url.pathname.startsWith("/api")) {
|
||||
//
|
||||
// NOTE: When tRPC hits an error a 500 is returned, when this is received
|
||||
// by the application the user is automatically redirected to /auth/login.
|
||||
//
|
||||
// - For this reason our matchers are sufficient for an app-wide maintenance page.
|
||||
//
|
||||
try {
|
||||
// Check whether the maintenance page should be shown
|
||||
const isInMaintenanceMode = await get<boolean>("isInMaintenanceMode");
|
||||
// If is in maintenance mode, point the url pathname to the maintenance page
|
||||
if (isInMaintenanceMode) {
|
||||
req.nextUrl.pathname = `/maintenance`;
|
||||
return NextResponse.rewrite(req.nextUrl);
|
||||
}
|
||||
} catch (error) {
|
||||
// show the default page if EDGE_CONFIG env var is missing,
|
||||
// but log the error to the console
|
||||
// console.error(error);
|
||||
}
|
||||
}
|
||||
|
||||
if (["/api/collect-events", "/api/auth"].some((p) => url.pathname.startsWith(p))) {
|
||||
const callbackUrl = url.searchParams.get("callbackUrl");
|
||||
const { isBot } = userAgent(req);
|
||||
@@ -36,11 +59,28 @@ const middleware: NextMiddleware = async (req) => {
|
||||
return NextResponse.rewrite(url);
|
||||
}
|
||||
|
||||
if (url.pathname.startsWith("/auth/login")) {
|
||||
const moreHeaders = new Headers(req.headers);
|
||||
// Use this header to actually enforce CSP, otherwise it is running in Report Only mode on all pages.
|
||||
moreHeaders.set("x-csp-enforce", "true");
|
||||
return NextResponse.next({
|
||||
request: {
|
||||
headers: moreHeaders,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return NextResponse.next();
|
||||
};
|
||||
|
||||
export const config = {
|
||||
matcher: ["/api/collect-events/:path*", "/api/auth/:path*", "/apps/routing_forms/:path*", "/:path*/embed"],
|
||||
matcher: [
|
||||
"/api/collect-events/:path*",
|
||||
"/api/auth/:path*",
|
||||
"/apps/routing_forms/:path*",
|
||||
"/:path*/embed",
|
||||
"/auth/login",
|
||||
],
|
||||
};
|
||||
|
||||
export default collectEvents({
|
||||
|
||||
@@ -35,6 +35,12 @@ if (!process.env.NEXT_PUBLIC_WEBSITE_URL) {
|
||||
process.env.NEXT_PUBLIC_WEBSITE_URL = process.env.NEXT_PUBLIC_WEBAPP_URL;
|
||||
}
|
||||
|
||||
if (process.env.CSP_POLICY === "strict" && process.env.NODE_ENV === "production") {
|
||||
throw new Error(
|
||||
"Strict CSP policy(for style-src) is not yet supported in production. You can experiment with it in Dev Mode"
|
||||
);
|
||||
}
|
||||
|
||||
if (!process.env.EMAIL_FROM) {
|
||||
console.warn(
|
||||
"\x1b[33mwarn",
|
||||
@@ -176,6 +182,41 @@ const nextConfig = {
|
||||
}, */
|
||||
];
|
||||
},
|
||||
async headers() {
|
||||
return [
|
||||
{
|
||||
source: "/auth/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: "/signup",
|
||||
headers: [
|
||||
{
|
||||
key: "X-Frame-Options",
|
||||
value: "DENY",
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
source: "/:path*",
|
||||
headers: [
|
||||
{
|
||||
key: "X-Content-Type-Options",
|
||||
value: "nosniff",
|
||||
},
|
||||
{
|
||||
key: "Referrer-Policy",
|
||||
value: "strict-origin-when-cross-origin",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
},
|
||||
async redirects() {
|
||||
const redirects = [
|
||||
{
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.5.3",
|
||||
"version": "2.5.11",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
@@ -23,7 +23,7 @@
|
||||
"yarn": ">=1.19.0 < 2.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@boxyhq/saml-jackson": "1.3.6",
|
||||
"@boxyhq/saml-jackson": "1.7.1",
|
||||
"@calcom/app-store": "*",
|
||||
"@calcom/app-store-cli": "*",
|
||||
"@calcom/core": "*",
|
||||
@@ -37,7 +37,7 @@
|
||||
"@calcom/trpc": "*",
|
||||
"@calcom/tsconfig": "*",
|
||||
"@calcom/ui": "*",
|
||||
"@daily-co/daily-js": "^0.26.0",
|
||||
"@daily-co/daily-js": "^0.37.0",
|
||||
"@formkit/auto-animate": "^1.0.0-beta.5",
|
||||
"@glidejs/glide": "^3.5.2",
|
||||
"@heroicons/react": "^1.0.6",
|
||||
@@ -60,6 +60,8 @@
|
||||
"@stripe/react-stripe-js": "^1.10.0",
|
||||
"@stripe/stripe-js": "^1.35.0",
|
||||
"@tanstack/react-query": "^4.3.9",
|
||||
"@types/turndown": "^5.0.1",
|
||||
"@vercel/edge-config": "^0.1.1",
|
||||
"@vercel/edge-functions-ui": "^0.2.1",
|
||||
"@vercel/og": "^0.0.21",
|
||||
"accept-language-parser": "^1.5.0",
|
||||
@@ -118,6 +120,7 @@
|
||||
"stripe": "^9.16.0",
|
||||
"superjson": "1.9.1",
|
||||
"tailwindcss-radix": "^2.6.0",
|
||||
"turndown": "^7.1.1",
|
||||
"uuid": "^8.3.2",
|
||||
"web3": "^1.7.5",
|
||||
"zod": "^3.20.2"
|
||||
@@ -162,6 +165,6 @@
|
||||
"tailwindcss": "^3.2.1",
|
||||
"ts-jest": "^28.0.8",
|
||||
"ts-node": "^10.9.1",
|
||||
"typescript": "^4.7.4"
|
||||
"typescript": "^4.9.4"
|
||||
}
|
||||
}
|
||||
|
||||
+13
-12
@@ -4,7 +4,8 @@ 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, HeadSeo } from "@calcom/ui";
|
||||
import { HeadSeo } from "@calcom/ui";
|
||||
import { FiFileText, FiCheck, FiBookOpen, FiChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useLocale } from "@lib/hooks/useLocale";
|
||||
|
||||
@@ -20,13 +21,13 @@ export default function Custom404() {
|
||||
{
|
||||
title: t("documentation"),
|
||||
description: t("documentation_description"),
|
||||
icon: Icon.FiFileText,
|
||||
icon: FiFileText,
|
||||
href: DOCS_URL,
|
||||
},
|
||||
{
|
||||
title: t("blog"),
|
||||
description: t("blog_description"),
|
||||
icon: Icon.FiBookOpen,
|
||||
icon: FiBookOpen,
|
||||
href: `${WEBSITE_URL}/blog`,
|
||||
},
|
||||
];
|
||||
@@ -75,7 +76,7 @@ export default function Custom404() {
|
||||
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-green-50">
|
||||
<Icon.FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -90,7 +91,7 @@ export default function Custom404() {
|
||||
<p className="text-base text-gray-500">{t("the_infrastructure_plan")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -103,7 +104,7 @@ export default function Custom404() {
|
||||
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" />
|
||||
<FiFileText className="h-6 w-6 text-gray-700" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -116,7 +117,7 @@ export default function Custom404() {
|
||||
<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" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -161,7 +162,7 @@ export default function Custom404() {
|
||||
<p className="text-base text-gray-500">{t("join_our_community")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -218,7 +219,7 @@ export default function Custom404() {
|
||||
rel="noreferrer">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||
<Icon.FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||
</span>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -233,7 +234,7 @@ export default function Custom404() {
|
||||
<p className="text-base text-gray-500">{t("claim_username_and_schedule_events")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
@@ -261,7 +262,7 @@ export default function Custom404() {
|
||||
<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" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -307,7 +308,7 @@ export default function Custom404() {
|
||||
<p className="text-base text-gray-500">{t("join_our_community")}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 self-center">
|
||||
<Icon.FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
<FiChevronRight className="h-5 w-5 text-gray-400" aria-hidden="true" />
|
||||
</div>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -3,7 +3,8 @@ import { useRouter } from "next/router";
|
||||
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, showToast } from "@calcom/ui";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { FiCopy } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function Error500() {
|
||||
const { t } = useLocale();
|
||||
@@ -33,7 +34,7 @@ export default function Error500() {
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||
StartIcon={Icon.FiCopy}
|
||||
StartIcon={FiCopy}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(router.query.error as string);
|
||||
showToast("Link copied!", "success");
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import classNames from "classnames";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
@@ -26,13 +27,16 @@ 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 { Icon, HeadSeo, AvatarGroup, Avatar } from "@calcom/ui";
|
||||
import { HeadSeo, AvatarGroup, Avatar } from "@calcom/ui";
|
||||
import { BadgeCheckIcon, FiArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
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
|
||||
@@ -40,6 +44,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
|
||||
const isBioEmpty = !user.bio || !user.bio.replace("<p><br></p>", "").length;
|
||||
|
||||
const groupEventTypes = props.users.some((user) => !user.allowDynamicBooking) ? (
|
||||
<div className="space-y-6" data-testid="event-types">
|
||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||
@@ -54,8 +60,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
{eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||
<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" />
|
||||
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||
<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 })}
|
||||
className="flex justify-between px-6 py-4"
|
||||
@@ -134,10 +140,17 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<h1 className="font-cal mb-1 text-3xl text-gray-900 dark:text-white">
|
||||
{nameOrUsername}
|
||||
{user.verified && (
|
||||
<Icon.BadgeCheckIcon className="mx-1 -mt-1 inline h-6 w-6 text-blue-500 dark:text-white" />
|
||||
<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-gray-500">{user.bio}</p>
|
||||
{!isBioEmpty && (
|
||||
<>
|
||||
<div
|
||||
className="dark:text-darkgray-600 text-sm text-gray-500 [&_a]:text-blue-500 [&_a]:underline [&_a]:hover:text-blue-600"
|
||||
dangerouslySetInnerHTML={{ __html: md.render(user.bio || "") }}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
@@ -162,8 +175,8 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<div
|
||||
key={type.id}
|
||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||
<Icon.FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
className="dark:bg-darkgray-100 group relative border-b border-gray-200 bg-white first:rounded-t-md last:rounded-b-md last:border-b-0 hover:bg-gray-50 dark:border-gray-700 dark:hover:border-gray-600">
|
||||
<FiArrowRight className="absolute right-4 top-4 h-4 w-4 text-black opacity-0 transition-opacity group-hover:opacity-100 dark:text-white" />
|
||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||
<Link
|
||||
prefetch={false}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import MarkdownIt from "markdown-it";
|
||||
import { GetStaticPaths, GetStaticPropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
import { z } from "zod";
|
||||
|
||||
import { privacyFilteredLocations, LocationObject } from "@calcom/app-store/locations";
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { JSONObject } from "superjson/dist/types";
|
||||
|
||||
import { LocationObject, privacyFilteredLocations } from "@calcom/app-store/locations";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
|
||||
+15
-1
@@ -29,13 +29,27 @@ function MyApp(props: AppProps) {
|
||||
} else if (router.pathname === "/500") {
|
||||
pageStatus = "500";
|
||||
}
|
||||
|
||||
// On client side don't let nonce creep into DOM
|
||||
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
||||
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
||||
const providerProps = {
|
||||
...props,
|
||||
pageProps: {
|
||||
...props.pageProps,
|
||||
nonce,
|
||||
},
|
||||
};
|
||||
// Use the layout defined at the page level, if available
|
||||
const getLayout = Component.getLayout ?? ((page) => page);
|
||||
return (
|
||||
<AppProviders {...props}>
|
||||
<AppProviders {...providerProps}>
|
||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||
<I18nLanguageHandler />
|
||||
<Script
|
||||
nonce={nonce}
|
||||
id="page-status"
|
||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||
/>
|
||||
|
||||
@@ -1,58 +1,36 @@
|
||||
import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps } from "next/document";
|
||||
import Script from "next/script";
|
||||
import { z } from "zod";
|
||||
|
||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
||||
|
||||
import { csp } from "@lib/csp";
|
||||
|
||||
type Props = Record<string, unknown> & DocumentProps;
|
||||
|
||||
function toRunBeforeReactOnClient() {
|
||||
const calEmbedMode = typeof new URL(document.URL).searchParams.get("embed") === "string";
|
||||
/* Iframe Name */
|
||||
window.name.includes("cal-embed");
|
||||
|
||||
window.isEmbed = () => {
|
||||
// Once an embed mode always an embed mode
|
||||
return calEmbedMode;
|
||||
};
|
||||
|
||||
window.resetEmbedStatus = () => {
|
||||
try {
|
||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
||||
window.sessionStorage.removeItem("calEmbedMode");
|
||||
} catch (e) {}
|
||||
};
|
||||
|
||||
window.getEmbedTheme = () => {
|
||||
const url = new URL(document.URL);
|
||||
return url.searchParams.get("theme") as "dark" | "light";
|
||||
};
|
||||
|
||||
window.getEmbedNamespace = () => {
|
||||
const url = new URL(document.URL);
|
||||
const namespace = url.searchParams.get("embed");
|
||||
return namespace;
|
||||
};
|
||||
|
||||
window.isPageOptimizedForEmbed = () => {
|
||||
// Those pages are considered optimized, which know at backend that they are rendering for embed.
|
||||
// Such pages can be shown straightaway without a loader for a better embed experience
|
||||
return location.pathname.includes("forms/");
|
||||
};
|
||||
}
|
||||
|
||||
class MyDocument extends Document<Props> {
|
||||
static async getInitialProps(ctx: DocumentContext) {
|
||||
const { nonce } = csp(ctx.req || null, ctx.res || null);
|
||||
if (!process.env.CSP_POLICY) {
|
||||
ctx.res?.setHeader("x-csp", "not-opted-in");
|
||||
} else if (!ctx.res?.getHeader("x-csp")) {
|
||||
// If x-csp not set by gSSP, then it's initialPropsOnly
|
||||
ctx.res?.setHeader("x-csp", "initialPropsOnly");
|
||||
}
|
||||
const isEmbed = ctx.asPath?.includes("/embed") || ctx.asPath?.includes("embedType=");
|
||||
const initialProps = await Document.getInitialProps(ctx);
|
||||
return { isEmbed, ...initialProps };
|
||||
return { isEmbed, nonce, ...initialProps };
|
||||
}
|
||||
|
||||
render() {
|
||||
const { locale } = this.props.__NEXT_DATA__;
|
||||
const { isEmbed } = this.props;
|
||||
const nonceParsed = z.string().safeParse(this.props.nonce);
|
||||
const nonce = nonceParsed.success ? nonceParsed.data : "";
|
||||
const dir = getDirFromLang(locale);
|
||||
return (
|
||||
<Html lang={locale} dir={dir}>
|
||||
<Head>
|
||||
<Head nonce={nonce}>
|
||||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||||
@@ -68,22 +46,13 @@ class MyDocument extends Document<Props> {
|
||||
crossOrigin="anonymous"
|
||||
/>
|
||||
<link rel="preload" href="/fonts/cal.ttf" as="font" type="font/ttf" crossOrigin="anonymous" />
|
||||
{/* Define isEmbed here so that it can be shared with App(embed-iframe) as well as the following code to change background and hide body
|
||||
Persist the embed mode in sessionStorage because query param might get lost during browsing.
|
||||
*/}
|
||||
<Script
|
||||
id="run-before-client"
|
||||
strategy="beforeInteractive"
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: `(${toRunBeforeReactOnClient.toString()})()`,
|
||||
}}
|
||||
/>
|
||||
<Script src="/embed-init-iframe.js" strategy="beforeInteractive" />
|
||||
</Head>
|
||||
|
||||
<body
|
||||
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100 antialiased"
|
||||
style={
|
||||
this.props.isEmbed
|
||||
isEmbed
|
||||
? {
|
||||
background: "transparent",
|
||||
// Keep the embed hidden till parent initializes and
|
||||
@@ -95,7 +64,7 @@ class MyDocument extends Document<Props> {
|
||||
: {}
|
||||
}>
|
||||
<Main />
|
||||
<NextScript />
|
||||
<NextScript nonce={nonce} />
|
||||
</body>
|
||||
</Html>
|
||||
);
|
||||
|
||||
@@ -2,6 +2,7 @@ import { IdentityProvider, UserPermissionRole } from "@prisma/client";
|
||||
import { readFileSync } from "fs";
|
||||
import Handlebars from "handlebars";
|
||||
import NextAuth, { Session } from "next-auth";
|
||||
import { encode } from "next-auth/jwt";
|
||||
import { Provider } from "next-auth/providers";
|
||||
import CredentialsProvider from "next-auth/providers/credentials";
|
||||
import EmailProvider from "next-auth/providers/email";
|
||||
@@ -17,14 +18,14 @@ import { ErrorCode, isPasswordValid, verifyPassword } from "@calcom/lib/auth";
|
||||
import { APP_NAME, IS_TEAM_BILLING_ENABLED, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||
import { randomString } from "@calcom/lib/random";
|
||||
import rateLimit from "@calcom/lib/rateLimit";
|
||||
import { serverConfig } from "@calcom/lib/serverConfig";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { teamMetadataSchema, userMetadata } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||
import { randomString } from "@lib/random";
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||
|
||||
@@ -60,6 +61,7 @@ const providers: Provider[] = [
|
||||
username: true,
|
||||
name: true,
|
||||
email: true,
|
||||
metadata: true,
|
||||
identityProvider: true,
|
||||
password: true,
|
||||
twoFactorEnabled: true,
|
||||
@@ -72,8 +74,9 @@ const providers: Provider[] = [
|
||||
},
|
||||
});
|
||||
|
||||
// Don't leak information about it being username or password that is invalid
|
||||
if (!user) {
|
||||
throw new Error(ErrorCode.UserNotFound);
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.identityProvider !== IdentityProvider.CAL) {
|
||||
@@ -81,12 +84,12 @@ const providers: Provider[] = [
|
||||
}
|
||||
|
||||
if (!user.password) {
|
||||
throw new Error(ErrorCode.UserMissingPassword);
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
const isCorrectPassword = await verifyPassword(credentials.password, user.password);
|
||||
if (!isCorrectPassword) {
|
||||
throw new Error(ErrorCode.IncorrectPassword);
|
||||
throw new Error(ErrorCode.IncorrectUsernamePassword);
|
||||
}
|
||||
|
||||
if (user.twoFactorEnabled) {
|
||||
@@ -161,6 +164,7 @@ if (IS_GOOGLE_LOGIN_ENABLED) {
|
||||
GoogleProvider({
|
||||
clientId: GOOGLE_CLIENT_ID,
|
||||
clientSecret: GOOGLE_CLIENT_SECRET,
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -199,6 +203,7 @@ if (isSAMLLoginEnabled) {
|
||||
clientId: "dummy",
|
||||
clientSecret: "dummy",
|
||||
},
|
||||
allowDangerousEmailAccountLinking: true,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -233,7 +238,13 @@ if (true) {
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function isNumber(n: string) {
|
||||
return !isNaN(parseFloat(n)) && !isNaN(+n);
|
||||
}
|
||||
|
||||
const calcomAdapter = CalComAdapter(prisma);
|
||||
|
||||
export default NextAuth({
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
@@ -241,6 +252,26 @@ export default NextAuth({
|
||||
session: {
|
||||
strategy: "jwt",
|
||||
},
|
||||
jwt: {
|
||||
// decorate the native JWT encode function
|
||||
// Impl. detail: We don't pass through as this function is called with encode/decode functions.
|
||||
encode: async ({ token, maxAge, secret }) => {
|
||||
if (token?.sub && isNumber(token.sub)) {
|
||||
const user = await prisma.user.findFirst({
|
||||
where: { id: Number(token.sub) },
|
||||
select: { metadata: true },
|
||||
});
|
||||
// if no user is found, we still don't want to crash here.
|
||||
if (user) {
|
||||
const metadata = userMetadata.parse(user.metadata);
|
||||
if (metadata?.sessionTimeout) {
|
||||
maxAge = metadata.sessionTimeout / 60;
|
||||
}
|
||||
}
|
||||
}
|
||||
return encode({ secret, token, maxAge });
|
||||
},
|
||||
},
|
||||
cookies: defaultCookies(WEBAPP_URL?.startsWith("https://")),
|
||||
pages: {
|
||||
signIn: "/auth/login",
|
||||
@@ -331,7 +362,7 @@ export default NextAuth({
|
||||
return token;
|
||||
},
|
||||
async session({ session, token }) {
|
||||
const hasValidLicense = await checkLicense(process.env.CALCOM_LICENSE_KEY || "");
|
||||
const hasValidLicense = await checkLicense(prisma);
|
||||
const calendsoSession: Session = {
|
||||
...session,
|
||||
hasValidLicense,
|
||||
@@ -468,7 +499,13 @@ export default NextAuth({
|
||||
return true;
|
||||
}
|
||||
|
||||
if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
||||
// User signs up with email/password and then tries to login with Google/SAML using the same email
|
||||
if (
|
||||
existingUserWithEmail.identityProvider === IdentityProvider.CAL &&
|
||||
(idP === IdentityProvider.GOOGLE || idP === IdentityProvider.SAML)
|
||||
) {
|
||||
return true;
|
||||
} else if (existingUserWithEmail.identityProvider === IdentityProvider.CAL) {
|
||||
return "/auth/error?error=use-password-login";
|
||||
}
|
||||
|
||||
|
||||
@@ -4,10 +4,9 @@ import { NextApiRequest, NextApiResponse } from "next";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendPasswordResetEmail } from "@calcom/emails";
|
||||
import { PASSWORD_RESET_EXPIRY_HOURS } from "@calcom/emails/templates/forgot-password-email";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const t = await getTranslation(req.body.language ?? "en", "common");
|
||||
|
||||
|
||||
@@ -0,0 +1,37 @@
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
// This is the callback endpoint for the OIDC provider
|
||||
// A team must set this endpoint in the OIDC provider's configuration
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "GET") {
|
||||
return res.status(400).send("Method not allowed");
|
||||
}
|
||||
|
||||
const { code, state } = req.query as {
|
||||
code: string;
|
||||
state: string;
|
||||
};
|
||||
|
||||
const { oauthController } = await jackson();
|
||||
|
||||
try {
|
||||
const { redirect_url } = await oauthController.oidcAuthzResponse({ code, state });
|
||||
|
||||
if (!redirect_url) {
|
||||
throw new HttpError({
|
||||
message: "No redirect URL found",
|
||||
statusCode: 500,
|
||||
});
|
||||
}
|
||||
|
||||
return res.redirect(302, redirect_url);
|
||||
} catch (err) {
|
||||
const { message, statusCode = 500 } = err as HttpError;
|
||||
|
||||
return res.status(statusCode).send(message);
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
import { IdentityProvider } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
import { NextApiRequest } from "next";
|
||||
import z from "zod";
|
||||
|
||||
import { isPasswordValid } from "@calcom/lib/auth";
|
||||
@@ -50,7 +50,7 @@ async function handler(req: NextApiRequest) {
|
||||
},
|
||||
});
|
||||
|
||||
return { message: "First admin user created successfuly." };
|
||||
return { message: "First admin user created successfully." };
|
||||
}
|
||||
|
||||
export default defaultHandler({
|
||||
|
||||
@@ -2,11 +2,10 @@ import { IdentityProvider } from "@prisma/client";
|
||||
import { NextApiRequest, NextApiResponse } from "next";
|
||||
|
||||
import { hashPassword } from "@calcom/lib/auth";
|
||||
import slugify from "@calcom/lib/slugify";
|
||||
import { closeComUpsertTeamUser } from "@calcom/lib/sync/SyncServiceManager";
|
||||
import prisma from "@calcom/prisma";
|
||||
|
||||
import slugify from "@lib/slugify";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
if (req.method !== "POST") {
|
||||
return;
|
||||
@@ -44,7 +43,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
||||
OR: [
|
||||
{ username },
|
||||
{
|
||||
AND: [{ email: userEmail }, { password: { not: null } }, { username: { not: null } }],
|
||||
AND: [{ email: userEmail }],
|
||||
},
|
||||
],
|
||||
},
|
||||
|
||||
@@ -4,11 +4,10 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { sendOrganizerRequestReminderEmail } from "@calcom/emails";
|
||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||
|
||||
import { getTranslation } from "@server/lib/i18n";
|
||||
|
||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||
const apiKey = req.headers.authorization || req.query.apiKey;
|
||||
if (process.env.CRON_API_KEY !== apiKey) {
|
||||
|
||||
@@ -19,6 +19,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
<Shell
|
||||
isPublic
|
||||
backPath="/apps"
|
||||
smallHeading
|
||||
heading={
|
||||
<>
|
||||
<Link
|
||||
@@ -33,8 +34,7 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
||||
</span>
|
||||
)}
|
||||
</>
|
||||
}
|
||||
large>
|
||||
}>
|
||||
<div className="mb-16">
|
||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
||||
{apps.map((app) => {
|
||||
|
||||
@@ -4,7 +4,8 @@ import Link from "next/link";
|
||||
import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Icon, SkeletonText } from "@calcom/ui";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { FiArrowLeft, FiArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
@@ -15,7 +16,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
<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" />
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -31,7 +32,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
<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" />
|
||||
<FiArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -12,10 +12,10 @@ import {
|
||||
AppStoreCategories,
|
||||
HorizontalTabItemProps,
|
||||
HorizontalTabs,
|
||||
Icon,
|
||||
TextField,
|
||||
PopularAppsSlider,
|
||||
} from "@calcom/ui";
|
||||
import { FiSearch } from "@calcom/ui/components/icon";
|
||||
|
||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||
|
||||
@@ -42,7 +42,7 @@ function AppsSearch({
|
||||
return (
|
||||
<TextField
|
||||
className="!border-gray-100 bg-gray-100 !pl-0 focus:!ring-offset-0"
|
||||
addOnLeading={<Icon.FiSearch className="h-4 w-4 text-gray-500" />}
|
||||
addOnLeading={<FiSearch className="h-4 w-4 text-gray-500" />}
|
||||
addOnClassname="!border-gray-100"
|
||||
containerClassName={classNames("focus:!ring-offset-0", className)}
|
||||
type="search"
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { useRouter } from "next/router";
|
||||
import { useReducer } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||
import { InstallAppButton } from "@calcom/app-store/components";
|
||||
import { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import DisconnectIntegration from "@calcom/features/apps/components/DisconnectIntegration";
|
||||
import DisconnectIntegrationModal from "@calcom/features/apps/components/DisconnectIntegrationModal";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { App } from "@calcom/types/App";
|
||||
@@ -13,16 +14,31 @@ import {
|
||||
Alert,
|
||||
Button,
|
||||
EmptyScreen,
|
||||
Icon,
|
||||
List,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
ShellSubHeading,
|
||||
DropdownMenuTrigger,
|
||||
DropdownMenuContent,
|
||||
Dropdown,
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiBarChart,
|
||||
FiCalendar,
|
||||
FiCreditCard,
|
||||
FiGrid,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiShare2,
|
||||
FiTrash,
|
||||
FiVideo,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||
|
||||
function ConnectOrDisconnectIntegrationButton(props: {
|
||||
@@ -31,8 +47,9 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||
isGlobal?: boolean;
|
||||
installed?: boolean;
|
||||
invalidCredentialIds?: number[];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
}) {
|
||||
const { type, credentialIds, isGlobal, installed } = props;
|
||||
const { type, credentialIds, isGlobal, installed, handleDisconnect } = props;
|
||||
const { t } = useLocale();
|
||||
const [credentialId] = credentialIds;
|
||||
|
||||
@@ -41,25 +58,24 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||
utils.viewer.integrations.invalidate();
|
||||
};
|
||||
|
||||
if (credentialId) {
|
||||
if (type === "stripe_payment") {
|
||||
return (
|
||||
<DisconnectIntegration
|
||||
credentialId={credentialId}
|
||||
trashIcon
|
||||
onSuccess={handleOpenChange}
|
||||
buttonProps={{ className: "border border-gray-300" }}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
if (credentialId || type === "stripe_payment" || isGlobal) {
|
||||
return (
|
||||
<DisconnectIntegration
|
||||
credentialId={credentialId}
|
||||
trashIcon
|
||||
onSuccess={handleOpenChange}
|
||||
buttonProps={{ className: "border border-gray-300" }}
|
||||
/>
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => handleDisconnect(credentialId)}
|
||||
disabled={isGlobal}
|
||||
StartIcon={FiTrash}>
|
||||
{t("remove_app")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</Dropdown>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -70,14 +86,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
/** We don't need to "Connect", just show that it's installed */
|
||||
if (isGlobal) {
|
||||
return (
|
||||
<div className="truncate px-3 py-2">
|
||||
<h3 className="text-sm font-medium text-gray-700">{t("default")}</h3>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<InstallAppButton
|
||||
type={type}
|
||||
@@ -94,58 +103,64 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
||||
interface IntegrationsContainerProps {
|
||||
variant?: typeof InstalledAppVariants[number];
|
||||
exclude?: typeof InstalledAppVariants[number][];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
}
|
||||
|
||||
interface IntegrationsListProps {
|
||||
variant?: IntegrationsContainerProps["variant"];
|
||||
data: RouterOutputs["viewer"]["integrations"];
|
||||
handleDisconnect: (credentialId: number) => void;
|
||||
}
|
||||
|
||||
const IntegrationsList = ({ data }: IntegrationsListProps) => {
|
||||
const IntegrationsList = ({ data, handleDisconnect }: IntegrationsListProps) => {
|
||||
return (
|
||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||
<List>
|
||||
{data.items
|
||||
.filter((item) => item.invalidCredentialIds)
|
||||
.map((item) => (
|
||||
<IntegrationListItem
|
||||
name={item.name}
|
||||
slug={item.slug}
|
||||
key={item.title}
|
||||
title={item.title}
|
||||
logo={item.logo}
|
||||
<AppListCard
|
||||
key={item.name}
|
||||
description={item.description}
|
||||
separate={true}
|
||||
isTemplate={item.isTemplate}
|
||||
title={item.name}
|
||||
logo={item.logo}
|
||||
isDefault={item.isGlobal}
|
||||
shouldHighlight
|
||||
slug={item.slug}
|
||||
invalidCredential={item.invalidCredentialIds.length > 0}
|
||||
actions={
|
||||
<div className="flex w-16 justify-end">
|
||||
<div className="flex justify-end">
|
||||
<ConnectOrDisconnectIntegrationButton
|
||||
credentialIds={item.credentialIds}
|
||||
type={item.type}
|
||||
isGlobal={item.isGlobal}
|
||||
installed
|
||||
invalidCredentialIds={item.invalidCredentialIds}
|
||||
handleDisconnect={handleDisconnect}
|
||||
/>
|
||||
</div>
|
||||
}>
|
||||
<AppSettings slug={item.slug} />
|
||||
</IntegrationListItem>
|
||||
</AppListCard>
|
||||
))}
|
||||
</List>
|
||||
);
|
||||
};
|
||||
|
||||
const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps): JSX.Element => {
|
||||
const IntegrationsContainer = ({
|
||||
variant,
|
||||
exclude,
|
||||
handleDisconnect,
|
||||
}: IntegrationsContainerProps): JSX.Element => {
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
||||
const emptyIcon = {
|
||||
calendar: Icon.FiCalendar,
|
||||
conferencing: Icon.FiVideo,
|
||||
automation: Icon.FiShare2,
|
||||
analytics: Icon.FiBarChart,
|
||||
payment: Icon.FiCreditCard,
|
||||
web3: Icon.FiBarChart,
|
||||
other: Icon.FiGrid,
|
||||
calendar: FiCalendar,
|
||||
conferencing: FiVideo,
|
||||
automation: FiShare2,
|
||||
analytics: FiBarChart,
|
||||
payment: FiCreditCard,
|
||||
web3: FiBarChart,
|
||||
other: FiGrid,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -169,12 +184,12 @@ const IntegrationsContainer = ({ variant, exclude }: IntegrationsContainerProps)
|
||||
: "/apps"
|
||||
}
|
||||
color="secondary"
|
||||
StartIcon={Icon.FiPlus}>
|
||||
StartIcon={FiPlus}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
/>
|
||||
<IntegrationsList data={data} variant={variant} />
|
||||
<IntegrationsList handleDisconnect={handleDisconnect} data={data} variant={variant} />
|
||||
</div>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
@@ -206,6 +221,11 @@ const querySchema = z.object({
|
||||
|
||||
type querySchemaType = z.infer<typeof querySchema>;
|
||||
|
||||
type ModalState = {
|
||||
isOpen: boolean;
|
||||
credentialId: null | number;
|
||||
};
|
||||
|
||||
export default function InstalledApps() {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -218,14 +238,43 @@ export default function InstalledApps() {
|
||||
"web3",
|
||||
];
|
||||
|
||||
const [data, updateData] = useReducer(
|
||||
(data: ModalState, partialData: Partial<ModalState>) => ({ ...data, ...partialData }),
|
||||
{
|
||||
isOpen: false,
|
||||
credentialId: null,
|
||||
}
|
||||
);
|
||||
|
||||
const handleModelClose = () => {
|
||||
updateData({ isOpen: false, credentialId: null });
|
||||
};
|
||||
|
||||
const handleDisconnect = (credentialId: number) => {
|
||||
updateData({ isOpen: true, credentialId });
|
||||
};
|
||||
|
||||
return (
|
||||
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
||||
{categoryList.includes(category) && <IntegrationsContainer variant={category} />}
|
||||
{category === "calendar" && <CalendarListContainer />}
|
||||
{category === "other" && (
|
||||
<IntegrationsContainer variant={category} exclude={[...categoryList, "calendar"]} />
|
||||
)}
|
||||
</InstalledAppsLayout>
|
||||
<>
|
||||
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
||||
{categoryList.includes(category) && (
|
||||
<IntegrationsContainer handleDisconnect={handleDisconnect} variant={category} />
|
||||
)}
|
||||
{category === "calendar" && <CalendarListContainer />}
|
||||
{category === "other" && (
|
||||
<IntegrationsContainer
|
||||
handleDisconnect={handleDisconnect}
|
||||
variant={category}
|
||||
exclude={[...categoryList, "calendar"]}
|
||||
/>
|
||||
)}
|
||||
</InstalledAppsLayout>
|
||||
<DisconnectIntegrationModal
|
||||
handleModelClose={handleModelClose}
|
||||
isOpen={data.isOpen}
|
||||
credentialId={data.credentialId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -4,7 +4,8 @@ import { useRouter } from "next/router";
|
||||
import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon, SkeletonText } from "@calcom/ui";
|
||||
import { Button, SkeletonText } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
@@ -28,7 +29,7 @@ export default function Error() {
|
||||
<AuthContainer title="" description="">
|
||||
<div>
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-red-100">
|
||||
<Icon.FiX className="h-6 w-6 text-red-600" />
|
||||
<FiX className="h-6 w-6 text-red-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
|
||||
@@ -9,15 +9,17 @@ 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 { ErrorCode, getSession } from "@calcom/lib/auth";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Alert, Button, EmailField, Icon, PasswordField } from "@calcom/ui";
|
||||
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ErrorCode, getSession } from "@lib/auth";
|
||||
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import withNonce, { WithNonceProps } from "@lib/withNonce";
|
||||
|
||||
import AddToHomescreen from "@components/AddToHomescreen";
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
@@ -39,7 +41,7 @@ export default function Login({
|
||||
isSAMLLoginEnabled,
|
||||
samlTenantID,
|
||||
samlProductID,
|
||||
}: inferSSRProps<typeof getServerSideProps>) {
|
||||
}: inferSSRProps<typeof _getServerSideProps> & WithNonceProps) {
|
||||
const { t } = useLocale();
|
||||
const router = useRouter();
|
||||
const methods = useForm<LoginValues>();
|
||||
@@ -51,8 +53,8 @@ export default function Login({
|
||||
|
||||
const errorMessages: { [key: string]: string } = {
|
||||
// [ErrorCode.SecondFactorRequired]: t("2fa_enabled_instructions"),
|
||||
[ErrorCode.IncorrectPassword]: `${t("incorrect_password")} ${t("please_try_again")}`,
|
||||
[ErrorCode.UserNotFound]: t("no_account_exists"),
|
||||
// Don't leak information about whether an email is registered or not
|
||||
[ErrorCode.IncorrectUsernamePassword]: t("incorrect_username_password"),
|
||||
[ErrorCode.IncorrectTwoFactorCode]: `${t("incorrect_2fa_code")} ${t("please_try_again")}`,
|
||||
[ErrorCode.InternalServerError]: `${t("something_went_wrong")} ${t("please_try_again_and_contact_us")}`,
|
||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||
@@ -85,7 +87,7 @@ export default function Login({
|
||||
setTwoFactorRequired(false);
|
||||
methods.setValue("totpCode", "");
|
||||
}}
|
||||
StartIcon={Icon.FiArrowLeft}
|
||||
StartIcon={FiArrowLeft}
|
||||
color="minimal">
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
@@ -148,7 +150,7 @@ export default function Login({
|
||||
</div>
|
||||
<PasswordField
|
||||
id="password"
|
||||
autoComplete="current-password"
|
||||
autoComplete="off"
|
||||
required
|
||||
className="mb-0"
|
||||
{...register("password")}
|
||||
@@ -204,7 +206,8 @@ export default function Login({
|
||||
);
|
||||
}
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
// TODO: Once we understand how to retrieve prop types automatically from getServerSideProps, remove this temporary variable
|
||||
const _getServerSideProps = async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
const ssr = await ssrInit(context);
|
||||
@@ -228,7 +231,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
props: {
|
||||
csrfToken: await getCsrfToken(context),
|
||||
@@ -239,4 +241,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
samlProductID,
|
||||
},
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
export const getServerSideProps = withNonce(_getServerSideProps);
|
||||
|
||||
@@ -5,7 +5,8 @@ import { useEffect } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Icon } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { FiCheck } from "@calcom/ui/components/icon";
|
||||
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -31,7 +32,7 @@ export default function Logout(props: Props) {
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<Icon.FiCheck className="h-6 w-6 text-green-600" />
|
||||
<FiCheck className="h-6 w-6 text-green-600" />
|
||||
</div>
|
||||
<div className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-lg font-medium leading-6 text-gray-900" id="modal-title">
|
||||
|
||||
@@ -1,40 +1,125 @@
|
||||
import { UserPermissionRole } from "@prisma/client";
|
||||
import { GetServerSidePropsContext } from "next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useState } from "react";
|
||||
|
||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||
import { getSession } from "@calcom/lib/auth";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { WizardForm } from "@calcom/ui";
|
||||
import { Meta, WizardForm } from "@calcom/ui";
|
||||
|
||||
import SetupFormStep1 from "./steps/SetupFormStep1";
|
||||
import StepDone from "./steps/StepDone";
|
||||
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
const [isLoadingStep1, setIsLoadingStep1] = useState(false);
|
||||
const shouldDisable = props.userCount !== 0;
|
||||
const router = useRouter();
|
||||
const [value, setValue] = useState(props.isFreeLicense ? "FREE" : "EE");
|
||||
const isFreeLicense = value === "FREE";
|
||||
const [isEnabledEE, setIsEnabledEE] = useState(!props.isFreeLicense);
|
||||
const setStep = (newStep: number) => {
|
||||
router.replace(`/auth/setup?step=${newStep || 1}`, undefined, { shallow: true });
|
||||
};
|
||||
|
||||
const steps = [
|
||||
const steps: React.ComponentProps<typeof WizardForm>["steps"] = [
|
||||
{
|
||||
title: t("administrator_user"),
|
||||
description: t("lets_create_first_administrator_user"),
|
||||
content: shouldDisable ? <StepDone /> : <SetupFormStep1 setIsLoading={setIsLoadingStep1} />,
|
||||
isLoading: isLoadingStep1,
|
||||
content: (setIsLoading) => (
|
||||
<AdminUser
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(2);
|
||||
}}
|
||||
onError={() => {
|
||||
setIsLoading(false);
|
||||
}}
|
||||
userCount={props.userCount}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description"),
|
||||
content: <AdminAppsList baseURL="/auth/setup?step=2" useQueryParam={true} />,
|
||||
isLoading: false,
|
||||
title: t("choose_a_license"),
|
||||
description: t("choose_license_description"),
|
||||
content: (setIsLoading) => {
|
||||
return (
|
||||
<ChooseLicense
|
||||
id="wizard-step-2"
|
||||
name="wizard-step-2"
|
||||
value={value}
|
||||
onChange={setValue}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
setStep(3);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
if (!isFreeLicense) {
|
||||
steps.push({
|
||||
title: t("step_enterprise_license"),
|
||||
description: t("step_enterprise_license_description"),
|
||||
content: (setIsLoading) => {
|
||||
const currentStep = 3;
|
||||
return (
|
||||
<EnterpriseLicense
|
||||
id={`wizard-step-${currentStep}`}
|
||||
name={`wizard-step-${currentStep}`}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
}}
|
||||
onSuccess={() => {
|
||||
setStep(currentStep + 1);
|
||||
}}
|
||||
onSuccessValidate={() => {
|
||||
setIsEnabledEE(true);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
isEnabled: isEnabledEE,
|
||||
});
|
||||
}
|
||||
|
||||
steps.push({
|
||||
title: t("enable_apps"),
|
||||
description: t("enable_apps_description"),
|
||||
contentClassname: "!pb-0 mb-[-1px]",
|
||||
content: (setIsLoading) => {
|
||||
const currentStep = isFreeLicense ? 3 : 4;
|
||||
return (
|
||||
<AdminAppsList
|
||||
classNames={{
|
||||
form: "mb-4 rounded-md bg-white px-0 pt-0 md:max-w-full",
|
||||
appCategoryNavigationContainer: " max-h-[400px] overflow-y-auto md:p-4",
|
||||
verticalTabsItem: "!w-48 md:p-4",
|
||||
}}
|
||||
baseURL={`/auth/setup?step=${currentStep}`}
|
||||
useQueryParam={true}
|
||||
onSubmit={() => {
|
||||
setIsLoading(true);
|
||||
router.replace("/");
|
||||
}}
|
||||
/>
|
||||
);
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<main className="flex items-center bg-gray-100 print:h-full">
|
||||
<Meta title={t("setup")} description={t("setup_description")} />
|
||||
<main className="flex items-center bg-gray-100 print:h-full md:h-screen">
|
||||
<WizardForm
|
||||
href="/auth/setup"
|
||||
steps={steps}
|
||||
@@ -42,6 +127,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
finishLabel={t("finish")}
|
||||
prevLabel={t("prev_step")}
|
||||
stepLabel={(currentStep, maxSteps) => t("current_step_of_total", { currentStep, maxSteps })}
|
||||
containerClassname="md:w-[800px]"
|
||||
/>
|
||||
</main>
|
||||
</>
|
||||
@@ -49,6 +135,7 @@ export default function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
}
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const ssr = await ssrInit(context);
|
||||
const userCount = await prisma.user.count();
|
||||
const { req } = context;
|
||||
const session = await getSession({ req });
|
||||
@@ -62,8 +149,30 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
||||
};
|
||||
}
|
||||
|
||||
let deploymentKey = await getDeploymentKey(prisma);
|
||||
|
||||
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey) {
|
||||
await prisma.deployment.upsert({
|
||||
where: { id: 1 },
|
||||
update: {
|
||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||
agreedLicenseAt: new Date(),
|
||||
},
|
||||
create: {
|
||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||
agreedLicenseAt: new Date(),
|
||||
},
|
||||
});
|
||||
deploymentKey = await getDeploymentKey(prisma);
|
||||
}
|
||||
|
||||
const isFreeLicense = deploymentKey === "";
|
||||
|
||||
return {
|
||||
props: {
|
||||
trpcState: ssr.dehydrate(),
|
||||
isFreeLicense,
|
||||
userCount,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -15,7 +15,6 @@ import type { Schedule as ScheduleType, TimeRange, WorkingHours } from "@calcom/
|
||||
import {
|
||||
Button,
|
||||
Form,
|
||||
Icon,
|
||||
Label,
|
||||
showToast,
|
||||
Skeleton,
|
||||
@@ -25,6 +24,7 @@ import {
|
||||
Tooltip,
|
||||
VerticalDivider,
|
||||
} from "@calcom/ui";
|
||||
import { FiInfo, FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
|
||||
@@ -51,17 +51,17 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
});
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
<div className="p-6">
|
||||
<h3 className="font-medium leading-6 text-gray-900">
|
||||
{t("date_overrides")}{" "}
|
||||
<Tooltip content={t("date_overrides_info")}>
|
||||
<span className="inline-block">
|
||||
<Icon.FiInfo />
|
||||
<FiInfo />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</h3>
|
||||
<p className="mb-4 text-sm text-gray-500 ltr:mr-4 rtl:ml-4">{t("date_overrides_subtitle")}</p>
|
||||
<div className="mt-1 space-y-2">
|
||||
<p className="mb-4 text-sm text-gray-500">{t("date_overrides_subtitle")}</p>
|
||||
<div className="space-y-2">
|
||||
<DateOverrideList
|
||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||
remove={remove}
|
||||
@@ -74,7 +74,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||
onChange={(ranges) => append({ ranges })}
|
||||
Trigger={
|
||||
<Button color="secondary" StartIcon={Icon.FiPlus} data-testid="add-override">
|
||||
<Button color="secondary" StartIcon={FiPlus} data-testid="add-override">
|
||||
Add an override
|
||||
</Button>
|
||||
}
|
||||
@@ -185,25 +185,29 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||
...values,
|
||||
});
|
||||
}}
|
||||
className="flex flex-col pb-16 sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1 divide-y divide-gray-200 rounded-md border">
|
||||
<div className=" py-5 sm:p-6">
|
||||
{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
|
||||
}
|
||||
/>
|
||||
)}
|
||||
className="flex flex-col sm:mx-0 xl:flex-row xl:space-x-6">
|
||||
<div className="flex-1 flex-row xl:mr-0">
|
||||
<div className="mb-6 rounded-md border">
|
||||
<div>
|
||||
{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>
|
||||
<div className="my-6 rounded-md border">
|
||||
{data?.workingHours && <DateOverride workingHours={data.workingHours} />}
|
||||
</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 className="xl:max-w-80 w-full pr-4 sm:ml-0 sm:mr-36 sm:p-0">
|
||||
<div>
|
||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||
{t("timezone")}
|
||||
@@ -223,8 +227,8 @@ export default function Availability({ schedule }: { schedule: number }) {
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
<hr className="my-8" />
|
||||
<div className="rounded-md">
|
||||
<hr className="my-6 mr-8" />
|
||||
<div className="hidden rounded-md md:block">
|
||||
<h3 className="text-sm font-medium text-gray-900">{t("something_doesnt_look_right")}</h3>
|
||||
<div className="mt-3 flex">
|
||||
<Button href="/availability/troubleshoot" color="secondary">
|
||||
|
||||
@@ -5,7 +5,8 @@ import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules"
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { EmptyScreen, Icon, showToast } from "@calcom/ui";
|
||||
import { EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
import { HttpError } from "@lib/core/http/error";
|
||||
@@ -76,7 +77,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
{schedules.length === 0 ? (
|
||||
<div className="flex justify-center">
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiClock}
|
||||
Icon={FiClock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
buttonRaw={<NewScheduleButton />}
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { Calendar } from "@calcom/features/calendars/weeklyview";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { yyyymmdd } from "@calcom/lib/date-fns";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
@@ -16,13 +18,13 @@ export interface IBusySlot {
|
||||
}
|
||||
|
||||
const AvailabilityView = ({ user }: { user: User }) => {
|
||||
const { t } = useLocale();
|
||||
const { date, setQuery: setSelectedDate } = useRouterQuery("date");
|
||||
const selectedDate = dayjs(date);
|
||||
const formattedSelectedDate = selectedDate.format("YYYY-MM-DD");
|
||||
|
||||
const { data, isLoading } = trpc.viewer.availability.user.useQuery(
|
||||
const { data } = trpc.viewer.availability.user.useQuery(
|
||||
{
|
||||
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
|
||||
username: user.username!,
|
||||
dateFrom: selectedDate.startOf("day").utc().format(),
|
||||
dateTo: selectedDate.endOf("day").utc().format(),
|
||||
@@ -45,70 +47,26 @@ const AvailabilityView = ({ user }: { user: User }) => {
|
||||
}, [] as IBusySlot[]) || [];
|
||||
|
||||
return (
|
||||
<div className="max-w-xl overflow-hidden rounded-md bg-white shadow">
|
||||
<div className="px-4 py-5 sm:p-6">
|
||||
{t("overview_of_day")}{" "}
|
||||
<input
|
||||
type="date"
|
||||
className="inline h-8 border-none p-0"
|
||||
defaultValue={formattedSelectedDate}
|
||||
onChange={(e) => {
|
||||
if (e.target.value) setSelectedDate(e.target.value);
|
||||
}}
|
||||
<div className="max-w-xl overflow-hidden rounded-md border border-gray-200 bg-white">
|
||||
<div className="px-4">
|
||||
<Calendar
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
events={[...(data?.busy || []), ...overrides]
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot) => ({
|
||||
// TODO: Modify the Calendar view to be better at displaying blocks.
|
||||
id: undefined,
|
||||
title:
|
||||
(slot.title ? `(${slot.title}) - ` : "") + (slot.source ? `(source: ${slot.source})` : ""),
|
||||
start: new Date(slot.start),
|
||||
end: new Date(slot.end),
|
||||
}))}
|
||||
onDateChange={(e) => setSelectedDate(yyyymmdd(e))}
|
||||
startDate={selectedDate.startOf("day").toDate()}
|
||||
endDate={selectedDate.endOf("day").toDate()}
|
||||
view="day"
|
||||
/>
|
||||
<small className="block text-gray-400">{t("hover_over_bold_times_tip")}</small>
|
||||
<div className="mt-4 space-y-4">
|
||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||
{t("your_day_starts_at")} {convertMinsToHrsMins(user.startTime)}
|
||||
</div>
|
||||
</div>
|
||||
{(() => {
|
||||
if (isLoading)
|
||||
return (
|
||||
<>
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
<SkeletonText className="block h-16 w-full" />
|
||||
</>
|
||||
);
|
||||
|
||||
if (data && (data.busy.length > 0 || overrides.length > 0))
|
||||
return [...data.busy, ...overrides]
|
||||
.sort((a: IBusySlot, b: IBusySlot) => (a.start > b.start ? -1 : 1))
|
||||
.map((slot: IBusySlot) => (
|
||||
<div
|
||||
key={dayjs(slot.start).format("HH:mm")}
|
||||
className="overflow-hidden rounded-md bg-gray-100"
|
||||
data-testid="troubleshooter-busy-time">
|
||||
<div className="px-4 py-5 text-black sm:p-6">
|
||||
{t("calendar_shows_busy_between")}{" "}
|
||||
<span className="font-medium text-gray-800" title={dayjs(slot.start).format("HH:mm")}>
|
||||
{dayjs(slot.start).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("and")}{" "}
|
||||
<span className="font-medium text-gray-800" title={dayjs(slot.end).format("HH:mm")}>
|
||||
{dayjs(slot.end).format("HH:mm")}
|
||||
</span>{" "}
|
||||
{t("on")} {dayjs(slot.start).format("D")}{" "}
|
||||
{t(dayjs(slot.start).format("MMMM").toLowerCase())} {dayjs(slot.start).format("YYYY")}
|
||||
{slot.title && ` - (${slot.title})`}
|
||||
{slot.source && <small>{` - (source: ${slot.source})`}</small>}
|
||||
</div>
|
||||
</div>
|
||||
));
|
||||
return (
|
||||
<div className="overflow-hidden rounded-md bg-gray-100">
|
||||
<div className="px-4 py-5 text-black sm:p-6">{t("calendar_no_busy_slots")}</div>
|
||||
</div>
|
||||
);
|
||||
})()}
|
||||
|
||||
<div className="bg-brand dark:bg-darkmodebrand overflow-hidden rounded-md">
|
||||
<div className="text-brandcontrast dark:text-darkmodebrandcontrast px-4 py-2 sm:px-6">
|
||||
{t("your_day_ends_at")} {convertMinsToHrsMins(user.endTime)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -125,11 +83,3 @@ export default function Troubleshoot() {
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function convertMinsToHrsMins(mins: number) {
|
||||
const h = Math.floor(mins / 60);
|
||||
const m = mins % 60;
|
||||
const hs = h < 10 ? "0" + h : h;
|
||||
const ms = m < 10 ? "0" + m : m;
|
||||
return `${hs}:${ms}`;
|
||||
}
|
||||
|
||||
@@ -11,7 +11,11 @@ import { RRule } from "rrule";
|
||||
import { z } from "zod";
|
||||
|
||||
import BookingPageTagManager from "@calcom/app-store/BookingPageTagManager";
|
||||
import { getEventLocationValue, getSuccessPageLocationMessage } from "@calcom/app-store/locations";
|
||||
import {
|
||||
getEventLocationValue,
|
||||
getSuccessPageLocationMessage,
|
||||
guessEventLocationType,
|
||||
} from "@calcom/app-store/locations";
|
||||
import { getEventTypeAppData } from "@calcom/app-store/utils";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import dayjs, { ConfigType } from "@calcom/dayjs";
|
||||
@@ -34,8 +38,10 @@ import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/t
|
||||
import { localStorage } from "@calcom/lib/webstorage";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Prisma } from "@calcom/prisma/client";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { Button, EmailInput, Icon, HeadSeo } from "@calcom/ui";
|
||||
import { Button, EmailInput, HeadSeo } from "@calcom/ui";
|
||||
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone } from "@lib/clock";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -142,7 +148,7 @@ function RedirectionToast({ url }: { url: string }) {
|
||||
setIsToastVisible(false);
|
||||
}}
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-green-600 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<Icon.FiX className="h-6 w-6 text-white" />
|
||||
<FiX className="h-6 w-6 text-white" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
@@ -168,6 +174,7 @@ const querySchema = z.object({
|
||||
reschedule: stringToBoolean,
|
||||
isSuccessBookingPage: z.string().optional(),
|
||||
formerTime: z.string().optional(),
|
||||
email: z.string().optional(),
|
||||
});
|
||||
|
||||
export default function Success(props: SuccessProps) {
|
||||
@@ -179,23 +186,26 @@ export default function Success(props: SuccessProps) {
|
||||
cancel: isCancellationMode,
|
||||
changes,
|
||||
formerTime,
|
||||
email,
|
||||
} = querySchema.parse(router.query);
|
||||
|
||||
if ((isCancellationMode || changes) && typeof window !== "undefined") {
|
||||
window.scrollTo(0, document.body.scrollHeight);
|
||||
}
|
||||
|
||||
const location: ReturnType<typeof getEventLocationValue> = Array.isArray(props.bookingInfo.location)
|
||||
? props.bookingInfo.location[0]
|
||||
const location: ReturnType<typeof getEventLocationValue> = props.bookingInfo.location
|
||||
? props.bookingInfo.location
|
||||
: // If there is no location set then we default to Cal Video
|
||||
"integrations:daily";
|
||||
|
||||
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
|
||||
props?.bookingInfo?.metadata || {}
|
||||
)?.videoCallUrl;
|
||||
|
||||
if (!location) {
|
||||
// Can't use logger.error because it throws error on client. stdout isn't available to it.
|
||||
console.error(`No location found `);
|
||||
}
|
||||
|
||||
const email = props.bookingInfo?.user?.email;
|
||||
const status = props.bookingInfo?.status;
|
||||
const reschedule = props.bookingInfo.status === BookingStatus.ACCEPTED;
|
||||
const cancellationReason = props.bookingInfo.cancellationReason;
|
||||
@@ -254,6 +264,7 @@ export default function Success(props: SuccessProps) {
|
||||
if (!sdkActionManager) return;
|
||||
// TODO: We should probably make it consistent with Webhook payload. Some data is not available here, as and when requirement comes we can add
|
||||
sdkActionManager.fire("bookingSuccessful", {
|
||||
booking: bookingInfo,
|
||||
eventType,
|
||||
date: date.toString(),
|
||||
duration: calculatedDuration,
|
||||
@@ -278,8 +289,8 @@ export default function Success(props: SuccessProps) {
|
||||
|
||||
function eventLink(): string {
|
||||
const optional: { location?: string } = {};
|
||||
if (location) {
|
||||
optional["location"] = location;
|
||||
if (locationVideoCallUrl) {
|
||||
optional["location"] = locationVideoCallUrl;
|
||||
}
|
||||
|
||||
const event = createEvent({
|
||||
@@ -329,12 +340,17 @@ export default function Success(props: SuccessProps) {
|
||||
);
|
||||
const customInputs = bookingInfo?.customInputs;
|
||||
|
||||
const locationToDisplay = getSuccessPageLocationMessage(location, t);
|
||||
const locationToDisplay = getSuccessPageLocationMessage(
|
||||
locationVideoCallUrl ? locationVideoCallUrl : location,
|
||||
t,
|
||||
bookingInfo.status
|
||||
);
|
||||
|
||||
const hasSMSAttendeeAction =
|
||||
eventType.workflows.find((workflowEventType) =>
|
||||
workflowEventType.workflow.steps.find((step) => step.action === WorkflowActions.SMS_ATTENDEE)
|
||||
) !== undefined;
|
||||
const providerName = guessEventLocationType(location)?.label;
|
||||
|
||||
return (
|
||||
<div className={isEmbed ? "" : "h-screen"} data-testid="success-page">
|
||||
@@ -356,7 +372,7 @@ export default function Success(props: SuccessProps) {
|
||||
<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")}
|
||||
<FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -401,10 +417,10 @@ export default function Success(props: SuccessProps) {
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
)}
|
||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||
<Icon.FiCheck className="h-5 w-5 text-green-600" />
|
||||
<FiCheck className="h-5 w-5 text-green-600" />
|
||||
)}
|
||||
{needsConfirmation && !isCancelled && <Icon.FiCalendar className="h-5 w-5 text-gray-900" />}
|
||||
{isCancelled && <Icon.FiX className="h-5 w-5 text-red-600" />}
|
||||
{needsConfirmation && !isCancelled && <FiCalendar className="h-5 w-5 text-gray-900" />}
|
||||
{isCancelled && <FiX className="h-5 w-5 text-red-600" />}
|
||||
</div>
|
||||
<div className="mt-6 mb-8 text-center last:mb-0">
|
||||
<h3
|
||||
@@ -487,8 +503,14 @@ export default function Success(props: SuccessProps) {
|
||||
<div className="mt-3 font-medium">{t("where")}</div>
|
||||
<div className="col-span-2 mt-3">
|
||||
{locationToDisplay.startsWith("http") ? (
|
||||
<a title="Meeting Link" href={locationToDisplay}>
|
||||
{locationToDisplay}
|
||||
<a
|
||||
href={locationToDisplay}
|
||||
target="_blank"
|
||||
title={locationToDisplay}
|
||||
className="flex items-center gap-2 text-gray-700 underline dark:text-gray-50"
|
||||
rel="noreferrer">
|
||||
{providerName || "Link"}
|
||||
<FiExternalLink className="inline h-4 w-4 text-gray-700 dark:text-white" />
|
||||
</a>
|
||||
) : (
|
||||
locationToDisplay
|
||||
@@ -514,7 +536,7 @@ export default function Success(props: SuccessProps) {
|
||||
<>
|
||||
{eventTypeCustomFound?.type === "RADIO" && (
|
||||
<>
|
||||
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
||||
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
||||
{eventTypeCustomFound.label}
|
||||
</div>
|
||||
<div className="col-span-3 mt-1 mb-2">
|
||||
@@ -538,7 +560,9 @@ export default function Success(props: SuccessProps) {
|
||||
)}
|
||||
{eventTypeCustomFound?.type !== "RADIO" && customInput !== "" && (
|
||||
<>
|
||||
<div className="col-span-3 mt-8 border-t pt-8 pr-3 font-medium">{key}</div>
|
||||
<div className="border-bookinglightest dark:border-darkgray-300 col-span-3 mt-8 border-t pt-8 pr-3 font-medium">
|
||||
{key}
|
||||
</div>
|
||||
<div className="col-span-3 mt-2 mb-2">
|
||||
{typeof customInput === "boolean" ? (
|
||||
<p>{customInput ? "true" : "false"}</p>
|
||||
@@ -629,8 +653,8 @@ export default function Success(props: SuccessProps) {
|
||||
.format("YYYYMMDDTHHmmss[Z]")}&text=${eventName}&details=${
|
||||
props.eventType.description
|
||||
}` +
|
||||
(typeof location === "string"
|
||||
? "&location=" + encodeURIComponent(location)
|
||||
(typeof locationVideoCallUrl === "string"
|
||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
||||
: "") +
|
||||
(props.eventType.recurringEvent
|
||||
? "&recur=" +
|
||||
@@ -658,7 +682,10 @@ export default function Success(props: SuccessProps) {
|
||||
date.utc().format() +
|
||||
"&subject=" +
|
||||
eventName
|
||||
) + (location ? "&location=" + location : "")
|
||||
) +
|
||||
(locationVideoCallUrl
|
||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
||||
: "")
|
||||
}
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
|
||||
target="_blank">
|
||||
@@ -682,7 +709,10 @@ export default function Success(props: SuccessProps) {
|
||||
date.utc().format() +
|
||||
"&subject=" +
|
||||
eventName
|
||||
) + (location ? "&location=" + location : "")
|
||||
) +
|
||||
(locationVideoCallUrl
|
||||
? "&location=" + encodeURIComponent(locationVideoCallUrl)
|
||||
: "")
|
||||
}
|
||||
className="mx-2 h-10 w-10 rounded-sm border border-gray-200 px-3 py-2 dark:border-gray-700 dark:text-white"
|
||||
target="_blank">
|
||||
@@ -733,7 +763,7 @@ export default function Success(props: SuccessProps) {
|
||||
<EmailInput
|
||||
name="email"
|
||||
id="email"
|
||||
defaultValue={email || ""}
|
||||
defaultValue={email}
|
||||
className="mr- focus:border-brand border-bookinglightest dark:border-darkgray-300 mt-0 block w-full rounded-none rounded-l-md border-gray-300 shadow-sm focus:ring-black dark:bg-black dark:text-white sm:text-sm"
|
||||
placeholder="rick.astley@cal.com"
|
||||
/>
|
||||
@@ -981,6 +1011,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
endTime: true,
|
||||
location: true,
|
||||
status: true,
|
||||
metadata: true,
|
||||
cancellationReason: true,
|
||||
user: {
|
||||
select: {
|
||||
|
||||
@@ -9,7 +9,8 @@ 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 { RouterOutputs, trpc } from "@calcom/trpc/react";
|
||||
import { Alert, Button, EmptyScreen, Icon } from "@calcom/ui";
|
||||
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
||||
import { FiCalendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||
|
||||
@@ -87,6 +88,8 @@ export default function Bookings() {
|
||||
return false;
|
||||
}
|
||||
shownBookings[booking.recurringEventId] = [booking];
|
||||
} else if (status === "upcoming") {
|
||||
return new Date(booking.startTime).toDateString() !== new Date().toDateString();
|
||||
}
|
||||
return true;
|
||||
};
|
||||
@@ -120,7 +123,7 @@ export default function Bookings() {
|
||||
<p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("today")}</p>
|
||||
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<tbody className="divide-y divide-gray-200 bg-white">
|
||||
<tbody className="divide-y divide-gray-200 bg-white" data-testid="today-bookings">
|
||||
<Fragment>
|
||||
{bookingsToday.map((booking: BookingOutput) => (
|
||||
<BookingListItem
|
||||
@@ -137,7 +140,6 @@ export default function Bookings() {
|
||||
</div>
|
||||
)}
|
||||
<div className="pt-2 xl:pt-0">
|
||||
{/* <p className="mb-2 text-xs font-medium uppercase leading-4 text-gray-500">{t("all")}</p> */}
|
||||
<div className="overflow-hidden rounded-md border border-gray-200">
|
||||
<table className="w-full max-w-full table-fixed">
|
||||
<tbody className="divide-y divide-gray-200 bg-white" data-testid="bookings">
|
||||
@@ -176,7 +178,7 @@ export default function Bookings() {
|
||||
{query.status === "success" && isEmpty && (
|
||||
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||
<EmptyScreen
|
||||
Icon={Icon.FiCalendar}
|
||||
Icon={FiCalendar}
|
||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||
description={t("no_status_bookings_yet_description", {
|
||||
status: t(status).toLowerCase(),
|
||||
|
||||
@@ -3,11 +3,11 @@ import { z } from "zod";
|
||||
|
||||
import { privacyFilteredLocations, LocationObject } from "@calcom/core/location";
|
||||
import { parseRecurringEvent } from "@calcom/lib";
|
||||
import { getWorkingHours } from "@calcom/lib/availability";
|
||||
import { availiblityPageEventTypeSelect } from "@calcom/prisma";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
|
||||
import { getWorkingHours } from "@lib/availability";
|
||||
import { GetBookingType } from "@lib/getBooking";
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user