Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8ecfe89a6f | ||
|
|
b1ced10626 | ||
|
|
b3ba89c074 | ||
|
|
77d339ae8f | ||
|
|
ac9b2d0577 | ||
|
|
03fd546e99 | ||
|
|
6484f1639c | ||
|
|
f934dc6f4f | ||
|
|
dd9d32815a | ||
|
|
5c2ad7827e | ||
|
|
5dc5eadf28 | ||
|
|
d6fc551950 | ||
|
|
5bb868d2c3 | ||
|
|
ec9b3a4261 | ||
|
|
ce04f78a56 | ||
|
|
9f362db60c | ||
|
|
7a93bec735 | ||
|
|
7b1e824a36 | ||
|
|
0746b1e372 | ||
|
|
39747a151c | ||
|
|
b6ae4529e9 | ||
|
|
8c76da8d79 | ||
|
|
4210f35e9c | ||
|
+9 |
eae60043bb | ||
|
|
bf34296864 | ||
|
|
d0a24f7c3c | ||
|
|
f009321deb | ||
|
|
4d04a4d9a6 | ||
|
|
888f60a94c | ||
|
|
d6c4970bf4 | ||
|
|
63ce4a68ac | ||
|
|
cddcd742f6 | ||
|
|
a7a82a6b51 | ||
|
|
1a104f172d | ||
|
|
142be70b89 | ||
|
|
5a0e4c8726 | ||
|
|
f2cdb75f91 | ||
|
|
352ce35b40 | ||
|
|
3fa52b0600 | ||
|
|
a4ff5d0cee | ||
|
|
5f1d20319a | ||
|
|
d597aa91e5 | ||
|
|
803ab6c2e9 | ||
|
|
0a23be4579 | ||
|
|
00c3e9680c | ||
|
|
68ae3c48a0 | ||
|
|
1c59c0dd06 | ||
|
|
2d5bf1ffc1 | ||
|
|
317909d20a | ||
|
|
16eb516a9b | ||
|
|
a0e93e1eeb | ||
|
|
b18eabc6e8 | ||
|
|
5305f31266 | ||
|
|
6232a111ef | ||
|
|
0ea58103c8 | ||
|
|
3d595e9299 | ||
|
|
f135f7baad | ||
|
|
9f1341e94e | ||
|
|
79df03de49 | ||
|
+9 |
b7892b10ef | ||
|
|
ff5fd43a9e | ||
|
|
34c13db872 | ||
|
+8 |
e044f963f8 | ||
|
|
0344130f68 | ||
|
|
5f78eceb89 | ||
|
|
39a3a1affb | ||
|
|
bc6f959c7a | ||
|
|
bb45b9617b | ||
|
+8 |
8725034b6c | ||
|
|
e53abd921e | ||
|
|
7bb3397503 | ||
|
|
72d19d1d62 | ||
|
|
2931f4f84a | ||
|
|
7756b9c3a1 | ||
|
|
477941545e | ||
|
|
237828e4ed | ||
|
|
e23f3b27bf | ||
|
|
ee084aa8bd | ||
|
|
8ece2525e5 | ||
|
|
865c67a962 | ||
|
|
b6be94fd14 | ||
|
|
f298865d9e | ||
|
|
9e85770435 | ||
|
|
1427c8e792 | ||
|
|
baa641d1f4 | ||
|
|
51e08a7642 | ||
|
|
373c255733 | ||
|
|
f4fb4ddad1 | ||
|
|
79c491986a | ||
|
|
759a89bb0c | ||
|
|
41689ecc92 | ||
|
|
9118ca328f | ||
|
|
8fc4d342fd | ||
|
|
7f2560e1e3 | ||
|
|
3e201c9fe8 | ||
|
|
5ab5af753a | ||
|
|
5c01467caa | ||
|
|
3cf47c2ee9 | ||
|
|
8272fcd5c7 | ||
|
|
95e3f55f26 | ||
|
|
431e4224fe | ||
|
|
59ef4fe459 | ||
|
|
4211ec1a82 | ||
|
|
2e169801e8 | ||
|
|
d0d793a719 | ||
|
|
05f303bfad | ||
|
+9 |
c1b41d8251 | ||
|
|
06732c5c6d | ||
|
|
aa038acafd |
No files matched your search
@@ -5,7 +5,7 @@ name: 'Chromatic'
|
|||||||
|
|
||||||
# Event for the workflow
|
# Event for the workflow
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
pull_request: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Check types
|
name: Check types
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: E2E App-Store Apps
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request_target: # So we can test on forks
|
pull_request: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: E2E Embed tests and booking flow(for non-embed as well)
|
name: E2E Embed tests and booking flow(for non-embed as well)
|
||||||
on:
|
on:
|
||||||
pull_request_target: # So we can test on forks
|
pull_request: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
# Embed e2e - tests verify booking flow which is applicable to non-embed case also. So, don't ignore apps/web changes.
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: E2E test
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request_target: # So we can test on forks
|
pull_request: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: "Pull Request Labeler"
|
name: "Pull Request Labeler"
|
||||||
on:
|
on:
|
||||||
- pull_request_target
|
- pull_request
|
||||||
concurrency:
|
concurrency:
|
||||||
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }}
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
name: Lint
|
name: Lint
|
||||||
on:
|
on:
|
||||||
pull_request_target:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ name: Unit tests
|
|||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
branches: [fixes/e2e-consolidation] # TODO: Remove this after merged in main
|
||||||
pull_request_target: # So we can test on forks
|
pull_request: # So we can test on forks
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths:
|
paths:
|
||||||
|
|||||||
@@ -37,7 +37,7 @@
|
|||||||
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
<a href="https://hub.docker.com/r/calendso/calendso"><img src="https://img.shields.io/docker/pulls/calendso/calendso"></a>
|
||||||
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
<a href="https://twitter.com/calcom"><img src="https://img.shields.io/twitter/follow/calcom?style=flat"></a>
|
||||||
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
<a href="https://twitch.tv/calcomtv"><img src="https://img.shields.io/twitch/status/calcomtv?style=flat"></a>
|
||||||
<a href="https://github.com/orgs/calcom/projects/1/views/25"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
<a href="https://github.com/orgs/calcom/projects/9"><img src="https://img.shields.io/badge/Help%20Wanted-Contribute-blue"></a>
|
||||||
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
<a href="https://cal.com/figma"><img src="https://img.shields.io/badge/Figma-Design%20System-blueviolet"></a>
|
||||||
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
<a href="https://calendso.slack.com/archives/C02BY67GMMW"><img src="https://img.shields.io/badge/translations-contribute-brightgreen" /></a>
|
||||||
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
<a href="https://www.contributor-covenant.org/version/1/4/code-of-conduct/ "><img src="https://img.shields.io/badge/Contributor%20Covenant-1.4-purple" /></a>
|
||||||
@@ -324,7 +324,7 @@ We have a list of [help wanted](https://github.com/orgs/calcom/projects/1/views/
|
|||||||
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
6. In the third page (Test Users), add the Google account(s) you'll using. Make sure the details are correct on the last page of the wizard and your consent screen will be configured.
|
||||||
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
|
7. Now select [Credentials](https://console.cloud.google.com/apis/credentials) from the side pane and then select Create Credentials. Select the OAuth Client ID option.
|
||||||
8. Select Web Application as the Application Type.
|
8. Select Web Application as the Application Type.
|
||||||
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` replacing Cal.com URL with the URI at which your application runs.
|
9. Under Authorized redirect URI's, select Add URI and then add the URI `<Cal.com URL>/api/integrations/googlecalendar/callback` and `<Cal.com URL>/api/auth/callback/google` replacing Cal.com URL with the URI at which your application runs.
|
||||||
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
10. The key will be created and you will be redirected back to the Credentials page. Select the newly generated client ID under OAuth 2.0 Client IDs.
|
||||||
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
11. Select Download JSON. Copy the contents of this file and paste the entire JSON string in the .env file as the value for GOOGLE_API_CREDENTIALS key.
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,20 @@
|
|||||||
|
import { ComponentMeta } from "@storybook/react";
|
||||||
|
|
||||||
|
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
|
||||||
|
|
||||||
|
export default {
|
||||||
|
title: "Toggle Group",
|
||||||
|
component: ToggleGroup,
|
||||||
|
} as ComponentMeta<typeof ToggleGroup>;
|
||||||
|
|
||||||
|
export const Default = () => {
|
||||||
|
return (
|
||||||
|
<ToggleGroup
|
||||||
|
defaultValue="12"
|
||||||
|
options={[
|
||||||
|
{ value: "12", label: "12h" },
|
||||||
|
{ value: "24", label: "24h" },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -6,13 +6,13 @@ import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
|||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { App as AppType } from "@calcom/types/App";
|
import { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, SkeletonButton } from "@calcom/ui";
|
import { Button, SkeletonButton } from "@calcom/ui";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import Shell from "@calcom/ui/Shell";
|
import Shell from "@calcom/ui/Shell";
|
||||||
import Badge from "@calcom/ui/v2/core/Badge";
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@components/integrations/DisconnectIntegration";
|
||||||
|
|
||||||
|
|||||||
+7
-5
@@ -23,11 +23,11 @@ const ImageOption = (optionProps: OptionProps<{ [key: string]: string; type: App
|
|||||||
type={data.type}
|
type={data.type}
|
||||||
render={(installProps) => {
|
render={(installProps) => {
|
||||||
return (
|
return (
|
||||||
<Button {...installProps} className="w-full" color="minimal">
|
<Button {...installProps} className="flex w-full align-top" color="minimal">
|
||||||
{data.image && (
|
{data.image && (
|
||||||
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
<img className="float-left mr-3 inline h-5 w-5" src={data.image} alt={data.label} />
|
||||||
)}
|
)}
|
||||||
<p>{`${t("add")} ${data.label}`}</p>
|
<p className="text-left">{`${t("add")} ${data.label}`}</p>
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
@@ -64,10 +64,12 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
<Select
|
<Select
|
||||||
name="additionalCalendar"
|
name="additionalCalendar"
|
||||||
placeholder={
|
placeholder={
|
||||||
<Button className="rounded-md" StartIcon={Icon.FiPlus} color="secondary">
|
<div className="flex justify-start text-black">
|
||||||
{t("add")}
|
<Icon.FiPlus className="-ml-1 h-4 w-4 ltr:mr-2 rtl:ml-2 rtl:-mr-1" />
|
||||||
</Button>
|
<p>{t("add")}</p>
|
||||||
|
</div>
|
||||||
}
|
}
|
||||||
|
className="min-w-44"
|
||||||
options={options}
|
options={options}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
isLoading={isLoading}
|
isLoading={isLoading}
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
|
|
||||||
import AppCard from "./AppCard";
|
|
||||||
|
|
||||||
export default function AllApps({ apps }: { apps: App[] }) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mb-16">
|
|
||||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("all_apps")}</h2>
|
|
||||||
<div className="grid-col-1 grid grid-cols-1 gap-3 md:grid-cols-3">
|
|
||||||
{apps.map((app) => (
|
|
||||||
<AppCard key={app.name} app={app} />
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -14,6 +14,8 @@ import { showToast, SkeletonText } from "@calcom/ui/v2";
|
|||||||
import { Button, SkeletonButton, Shell } from "@calcom/ui/v2";
|
import { Button, SkeletonButton, Shell } from "@calcom/ui/v2";
|
||||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||||
|
|
||||||
|
import HeadSeo from "@components/seo/head-seo";
|
||||||
|
|
||||||
const Component = ({
|
const Component = ({
|
||||||
name,
|
name,
|
||||||
type,
|
type,
|
||||||
@@ -66,7 +68,7 @@ const Component = ({
|
|||||||
return (
|
return (
|
||||||
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
<div className="relative flex-1 flex-col items-start justify-start px-4 md:flex md:px-8 lg:flex-row lg:px-0">
|
||||||
{hasImages && (
|
{hasImages && (
|
||||||
<div className="flex-2 mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:rounded-md">
|
<div className="align-center mb-4 -ml-4 -mr-4 flex min-h-[450px] w-auto basis-3/5 snap-x snap-mandatory flex-row overflow-auto whitespace-nowrap bg-gray-100 p-4 md:mb-8 md:-ml-8 md:-mr-8 md:p-8 lg:mx-0 lg:mb-0 lg:max-w-2xl lg:flex-col lg:justify-center lg:rounded-md">
|
||||||
{images ? (
|
{images ? (
|
||||||
images.map((img) => (
|
images.map((img) => (
|
||||||
<img
|
<img
|
||||||
@@ -83,7 +85,7 @@ const Component = ({
|
|||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sticky top-0 -mt-4 max-w-xl flex-1 pb-12 text-sm lg:pb-0",
|
"sticky top-0 -mt-4 max-w-xl basis-2/5 pb-12 text-sm lg:pb-0",
|
||||||
hasImages && "lg:ml-8"
|
hasImages && "lg:ml-8"
|
||||||
)}>
|
)}>
|
||||||
<div className="mb-8 flex pt-4">
|
<div className="mb-8 flex pt-4">
|
||||||
@@ -276,6 +278,7 @@ const Component = ({
|
|||||||
|
|
||||||
export default function App(props: {
|
export default function App(props: {
|
||||||
name: string;
|
name: string;
|
||||||
|
description: AppType["description"];
|
||||||
type: AppType["type"];
|
type: AppType["type"];
|
||||||
isGlobal?: AppType["isGlobal"];
|
isGlobal?: AppType["isGlobal"];
|
||||||
logo: string;
|
logo: string;
|
||||||
@@ -300,7 +303,16 @@ export default function App(props: {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell large isPublic heading={t("app_store")} backPath="/apps">
|
<Shell large 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 ? (
|
{props.licenseRequired ? (
|
||||||
<LicenseRequired>
|
<LicenseRequired>
|
||||||
<Component {...props} />
|
<Component {...props} />
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import { App } from "@calcom/types/App";
|
|
||||||
import Badge from "@calcom/ui/Badge";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
|
|
||||||
interface AppCardProps {
|
|
||||||
app: App;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function AppCard(props: AppCardProps) {
|
|
||||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
|
||||||
return (
|
|
||||||
<Link href={"/apps/" + props.app.slug}>
|
|
||||||
<a
|
|
||||||
className="block h-full rounded-sm border border-gray-300 p-5 hover:bg-neutral-50"
|
|
||||||
data-testid={`app-store-app-card-${props.app.slug}`}>
|
|
||||||
<div className="flex">
|
|
||||||
{
|
|
||||||
// eslint-disable-next-line @next/next/no-img-element
|
|
||||||
<img src={props.app.logo} alt={props.app.name + " Logo"} className="mb-4 h-12 w-12 rounded-sm" />
|
|
||||||
}
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
className="ml-auto flex self-start"
|
|
||||||
onClick={() => {
|
|
||||||
console.log("The magic is supposed to happen here");
|
|
||||||
}}>
|
|
||||||
Add
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div className="flex items-center">
|
|
||||||
<h3 className="font-medium">{props.app.name}</h3>
|
|
||||||
</div>
|
|
||||||
{/* TODO: add reviews <div className="flex text-sm text-gray-800">
|
|
||||||
<span>{props.rating} stars</span> <StarIcon className="ml-1 mt-0.5 h-4 w-4 text-yellow-600" />
|
|
||||||
<span className="pl-1 text-gray-500">{props.reviews} reviews</span>
|
|
||||||
</div> */}
|
|
||||||
<p className="mt-2 truncate text-sm text-gray-500">{props.app.description}</p>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
+5
-5
@@ -3,8 +3,8 @@ import Link from "next/link";
|
|||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
||||||
@@ -12,14 +12,14 @@ import { Alert, Button, EmptyScreen } from "@calcom/ui/v2";
|
|||||||
import { List } from "@calcom/ui/v2/core/List";
|
import { List } from "@calcom/ui/v2/core/List";
|
||||||
import { ShellSubHeading } from "@calcom/ui/v2/core/Shell";
|
import { ShellSubHeading } from "@calcom/ui/v2/core/Shell";
|
||||||
import Switch from "@calcom/ui/v2/core/Switch";
|
import Switch from "@calcom/ui/v2/core/Switch";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
|
import AdditionalCalendarSelector from "@components/apps/AdditionalCalendarSelector";
|
||||||
|
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||||
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
import SubHeadingTitleWithConnections from "@components/integrations/SubHeadingTitleWithConnections";
|
||||||
import AdditionalCalendarSelector from "@components/v2/apps/AdditionalCalendarSelector";
|
|
||||||
import DestinationCalendarSelector from "@components/v2/apps/DestinationCalendarSelector";
|
|
||||||
import IntegrationListItem from "@components/v2/apps/IntegrationListItem";
|
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onChanged: () => unknown | Promise<unknown>;
|
onChanged: () => unknown | Promise<unknown>;
|
||||||
@@ -287,7 +287,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
|
<h1 className="text-sm font-semibold">{t("create_events_on")}</h1>
|
||||||
<p className="text-sm font-normal">{t("set_calendar")}</p>
|
<p className="text-sm font-normal">{t("set_calendar")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex justify-end md:w-6/12">
|
<div className="justify-end md:w-6/12">
|
||||||
<DestinationCalendarSelector
|
<DestinationCalendarSelector
|
||||||
onChange={mutation.mutate}
|
onChange={mutation.mutate}
|
||||||
hidePlaceholder
|
hidePlaceholder
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
import Image from "next/image";
|
|
||||||
import Link from "next/link";
|
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
|
|
||||||
export default function AppStoreCategories({
|
|
||||||
categories,
|
|
||||||
}: {
|
|
||||||
categories: {
|
|
||||||
name: string;
|
|
||||||
count: number;
|
|
||||||
}[];
|
|
||||||
}) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
return (
|
|
||||||
<div className="mb-16">
|
|
||||||
<h2 className="mb-2 text-lg font-semibold text-gray-900">{t("popular_categories")}</h2>
|
|
||||||
<div className="grid-col-1 grid w-full gap-3 overflow-scroll sm:grid-flow-col">
|
|
||||||
{categories.map((category) => (
|
|
||||||
<Link key={category.name} href={"/apps/categories/" + category.name}>
|
|
||||||
<a
|
|
||||||
data-testid={`app-store-category-${category.name}`}
|
|
||||||
className="relative flex rounded-sm bg-gray-100 px-6 py-4 sm:block">
|
|
||||||
<div className="min-w-24 -ml-5 text-center sm:ml-0">
|
|
||||||
<Image
|
|
||||||
alt={category.name}
|
|
||||||
width="352"
|
|
||||||
height="252"
|
|
||||||
layout="responsive"
|
|
||||||
src={"/app-store/" + category.name + ".svg"}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="self-center">
|
|
||||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
|
||||||
<p className="text-sm text-gray-500">{category.count} apps</p>
|
|
||||||
</div>
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
))}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
File renamed without changes.
+13
@@ -2,6 +2,8 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { ReactNode, useEffect, useState } from "react";
|
import { ReactNode, useEffect, useState } from "react";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { Icon } from "@calcom/ui";
|
||||||
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
|
import { ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/core/List";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
@@ -17,7 +19,9 @@ function IntegrationListItem(props: {
|
|||||||
logo: string;
|
logo: string;
|
||||||
destination?: boolean;
|
destination?: boolean;
|
||||||
separate?: boolean;
|
separate?: boolean;
|
||||||
|
invalidCredential?: boolean;
|
||||||
}): JSX.Element {
|
}): JSX.Element {
|
||||||
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { hl } = router.query;
|
const { hl } = router.query;
|
||||||
const [highlight, setHighlight] = useState(hl === props.slug);
|
const [highlight, setHighlight] = useState(hl === props.slug);
|
||||||
@@ -44,6 +48,15 @@ function IntegrationListItem(props: {
|
|||||||
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
<Link href={"/apps/" + props.slug}>{props.name || title}</Link>
|
||||||
</ListItemTitle>
|
</ListItemTitle>
|
||||||
<ListItemText component="p">{props.description}</ListItemText>
|
<ListItemText component="p">{props.description}</ListItemText>
|
||||||
|
{/* Alert error that key stopped working. */}
|
||||||
|
{props.invalidCredential && (
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<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>
|
||||||
<div>{props.actions}</div>
|
<div>{props.actions}</div>
|
||||||
</div>
|
</div>
|
||||||
File renamed without changes.
@@ -1,79 +0,0 @@
|
|||||||
import Glide, { Options } from "@glidejs/glide";
|
|
||||||
import "@glidejs/glide/dist/css/glide.core.min.css";
|
|
||||||
import "@glidejs/glide/dist/css/glide.theme.min.css";
|
|
||||||
import { useEffect, useRef } from "react";
|
|
||||||
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
|
||||||
|
|
||||||
const Slider = <T extends string | unknown>({
|
|
||||||
title = "",
|
|
||||||
className = "",
|
|
||||||
items,
|
|
||||||
itemKey = (item) => `${item}`,
|
|
||||||
renderItem,
|
|
||||||
options = {},
|
|
||||||
}: {
|
|
||||||
title?: string;
|
|
||||||
className?: string;
|
|
||||||
items: T[];
|
|
||||||
itemKey?: (item: T) => string;
|
|
||||||
renderItem?: (item: T) => JSX.Element;
|
|
||||||
options?: Options;
|
|
||||||
}) => {
|
|
||||||
const glide = useRef(null);
|
|
||||||
const slider = useRef<Glide.Properties | null>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (glide.current) {
|
|
||||||
slider.current = new Glide(glide.current, {
|
|
||||||
type: "carousel",
|
|
||||||
...options,
|
|
||||||
}).mount();
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => slider.current?.destroy();
|
|
||||||
}, [options]);
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={`mb-2 ${className}`}>
|
|
||||||
<style jsx global>
|
|
||||||
{`
|
|
||||||
.glide__slide {
|
|
||||||
height: auto !important;
|
|
||||||
}
|
|
||||||
`}
|
|
||||||
</style>
|
|
||||||
<div className="glide" ref={glide}>
|
|
||||||
<div className="flex cursor-default">
|
|
||||||
{title && (
|
|
||||||
<div>
|
|
||||||
<h2 className="mt-0 mb-2 text-lg font-semibold text-gray-900">{title}</h2>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
<div className="glide__arrows ml-auto" data-glide-el="controls">
|
|
||||||
<button data-glide-dir="<" className="mr-4">
|
|
||||||
<Icon.FiArrowLeft className="h-5 w-5 text-gray-600 hover:text-black" />
|
|
||||||
</button>
|
|
||||||
<button data-glide-dir=">">
|
|
||||||
<Icon.FiArrowRight className="h-5 w-5 text-gray-600 hover:text-black" />
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="glide__track" data-glide-el="track">
|
|
||||||
<ul className="glide__slides">
|
|
||||||
{items.map((item) => {
|
|
||||||
if (typeof renderItem !== "function") return null;
|
|
||||||
return (
|
|
||||||
<li key={itemKey(item)} className="glide__slide h-auto pl-0">
|
|
||||||
{renderItem(item)}
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default Slider;
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import type { App } from "@calcom/types/App";
|
|
||||||
|
|
||||||
import AppCard from "./AppCard";
|
|
||||||
import Slider from "./Slider";
|
|
||||||
|
|
||||||
const TrendingAppsSlider = <T extends App>({ items }: { items: T[] }) => {
|
|
||||||
const { t } = useLocale();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Slider<T>
|
|
||||||
className="mb-16"
|
|
||||||
title={t("trending_apps")}
|
|
||||||
items={items.filter((app) => !!app.trending)}
|
|
||||||
itemKey={(app) => app.name}
|
|
||||||
options={{
|
|
||||||
perView: 3,
|
|
||||||
breakpoints: {
|
|
||||||
768 /* and below */: {
|
|
||||||
perView: 1,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
renderItem={(app) => <AppCard app={app} />}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
/** @deprecated use v2 instead, located at packages/ui/v2/core/apps */
|
|
||||||
export default TrendingAppsSlider;
|
|
||||||
@@ -1,62 +0,0 @@
|
|||||||
import { signIn } from "next-auth/react";
|
|
||||||
import { Dispatch, SetStateAction } from "react";
|
|
||||||
import { useFormContext } from "react-hook-form";
|
|
||||||
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
|
||||||
import Button from "@calcom/ui/Button";
|
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
email: string;
|
|
||||||
samlTenantID: string;
|
|
||||||
samlProductID: string;
|
|
||||||
hostedCal: boolean;
|
|
||||||
setErrorMessage: Dispatch<SetStateAction<string | null>>;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function SAMLLogin(props: Props) {
|
|
||||||
const { t } = useLocale();
|
|
||||||
const methods = useFormContext();
|
|
||||||
const telemetry = useTelemetry();
|
|
||||||
|
|
||||||
const mutation = trpc.useMutation("viewer.public.samlTenantProduct", {
|
|
||||||
onSuccess: async (data) => {
|
|
||||||
await signIn("saml", {}, { tenant: data.tenant, product: data.product });
|
|
||||||
},
|
|
||||||
onError: (err) => {
|
|
||||||
props.setErrorMessage(err.message);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className="mt-5">
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
data-testid="saml"
|
|
||||||
className="flex w-full justify-center"
|
|
||||||
onClick={async (event) => {
|
|
||||||
event.preventDefault();
|
|
||||||
|
|
||||||
// track Google logins. Without personal data/payload
|
|
||||||
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
|
||||||
|
|
||||||
if (!props.hostedCal) {
|
|
||||||
await signIn("saml", {}, { tenant: props.samlTenantID, product: props.samlProductID });
|
|
||||||
} else {
|
|
||||||
if (props.email.length === 0) {
|
|
||||||
props.setErrorMessage(t("saml_email_required"));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// hosted solution, fetch tenant and product from the backend
|
|
||||||
mutation.mutate({
|
|
||||||
email: methods.getValues("email"),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}>
|
|
||||||
{t("signin_with_saml")}
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -2,12 +2,12 @@ import { useRouter } from "next/router";
|
|||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogTrigger } from "@calcom/ui/Dialog";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
import { Form } from "@calcom/ui/form/fields";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
|||||||
@@ -1,33 +1,41 @@
|
|||||||
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
import { getEventLocationType, locationKeyToString } from "@calcom/app-store/locations";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
|
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
||||||
|
|
||||||
import { Props } from "./pages/AvailabilityPage";
|
import { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
export function AvailableEventLocations({ locations }: { locations: Props["eventType"]["locations"] }) {
|
||||||
return locations.length ? (
|
return locations.length ? (
|
||||||
<div>
|
<div className="dark:text-darkgray-600 mr-6 flex w-full flex-col space-y-2 break-words text-sm text-gray-600">
|
||||||
<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) => {
|
||||||
{locations.map((location) => {
|
const eventLocationType = getEventLocationType(location.type);
|
||||||
const eventLocationType = getEventLocationType(location.type);
|
if (!eventLocationType) {
|
||||||
if (!eventLocationType) {
|
// It's possible that the location app got uninstalled
|
||||||
// It's possible that the location app got uninstalled
|
return null;
|
||||||
return null;
|
}
|
||||||
}
|
return (
|
||||||
return (
|
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
||||||
<div key={location.type} className="flex flex-row items-center text-sm font-medium">
|
<img
|
||||||
<img
|
src={eventLocationType.iconUrl}
|
||||||
src={eventLocationType.iconUrl}
|
className={classNames(
|
||||||
className={classNames(
|
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
||||||
"mr-[10px] ml-[2px] h-4 w-4 opacity-70 dark:opacity-100 ",
|
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
||||||
!eventLocationType.iconUrl?.includes("api") ? "dark:invert" : ""
|
)}
|
||||||
)}
|
alt={`${eventLocationType.label} icon`}
|
||||||
alt={`${eventLocationType.label} icon`}
|
/>
|
||||||
/>
|
<Tooltip content={locationKeyToString(location)}>
|
||||||
<span key={location.type}>{locationKeyToString(location)} </span>
|
<a
|
||||||
</div>
|
target="_blank"
|
||||||
);
|
href={locationKeyToString(location) ?? "/"}
|
||||||
})}
|
className="truncate"
|
||||||
</div>
|
key={location.type}
|
||||||
|
rel="noreferrer">
|
||||||
|
{locationKeyToString(location)}
|
||||||
|
</a>
|
||||||
|
</Tooltip>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<></>
|
<></>
|
||||||
|
|||||||
@@ -4,15 +4,18 @@ import { FC, useEffect, useState } from "react";
|
|||||||
|
|
||||||
import dayjs, { Dayjs } from "@calcom/dayjs";
|
import dayjs, { Dayjs } from "@calcom/dayjs";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
|
import { ToggleGroup } from "@calcom/ui/v2/core/form/ToggleGroup";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
|
|
||||||
type AvailableTimesProps = {
|
type AvailableTimesProps = {
|
||||||
timeFormat: string;
|
timeFormat: TimeFormat;
|
||||||
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||||
eventTypeId: number;
|
eventTypeId: number;
|
||||||
recurringCount: number | undefined;
|
recurringCount: number | undefined;
|
||||||
eventTypeSlug: string;
|
eventTypeSlug: string;
|
||||||
@@ -31,6 +34,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
eventTypeSlug,
|
eventTypeSlug,
|
||||||
recurringCount,
|
recurringCount,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
|
onTimeFormatChange,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
}) => {
|
}) => {
|
||||||
@@ -48,14 +52,25 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
<div className="dark:bg-darkgray-100 mt-8 flex h-full w-full flex-col px-4 text-center sm:mt-0 sm:p-5 md:-mb-5 md:min-w-[200px] lg:min-w-[300px]">
|
||||||
<div className="mb-4 text-left text-base">
|
<div className="mb-6 flex items-center text-left text-base">
|
||||||
<span className="text-bookingdarker dark:text-darkgray-800 mb-8 w-1/2 break-words font-semibold text-gray-900">
|
<div className="mr-4">
|
||||||
{nameOfDay(i18n.language, Number(date.format("d")))}
|
<span className="text-bookingdarker dark:text-darkgray-800 font-semibold text-gray-900">
|
||||||
</span>
|
{nameOfDay(i18n.language, Number(date.format("d")), "short")}
|
||||||
<span className="text-bookinglight font-medium">
|
</span>
|
||||||
{date.format(", D ")}
|
<span className="text-bookinglight font-medium">
|
||||||
{date.toDate().toLocaleString(i18n.language, { month: "long" })}
|
, {date.toDate().toLocaleString(i18n.language, { month: "long" })} {date.format(" D ")}
|
||||||
</span>
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="ml-auto">
|
||||||
|
<ToggleGroup
|
||||||
|
onValueChange={(timeFormat) => onTimeFormatChange(timeFormat === "24")}
|
||||||
|
defaultValue={timeFormat === TimeFormat.TWELVE_HOUR ? "12" : "24"}
|
||||||
|
options={[
|
||||||
|
{ value: "12", label: t("12_hour_short") },
|
||||||
|
{ value: "24", label: t("24_hour_short") },
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
<div className="-mb-5 grid flex-grow grid-cols-1 gap-x-2 overflow-y-auto sm:block md:h-[364px]">
|
||||||
{slots.length > 0 &&
|
{slots.length > 0 &&
|
||||||
@@ -65,7 +80,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
query: Record<string, string | number | string[] | undefined>;
|
query: Record<string, string | number | string[] | undefined>;
|
||||||
};
|
};
|
||||||
const bookingUrl: BookingURL = {
|
const bookingUrl: BookingURL = {
|
||||||
pathname: "book",
|
pathname: router.pathname.endsWith("/embed") ? "../book" : "book",
|
||||||
query: {
|
query: {
|
||||||
...router.query,
|
...router.query,
|
||||||
date: dayjs(slot.time).format(),
|
date: dayjs(slot.time).format(),
|
||||||
|
|||||||
@@ -70,7 +70,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
<div className="max-w-[calc(100%_-_2rem)] flex-shrink break-words">
|
||||||
|
<EventTypeDescriptionSafeHTML eventType={eventType} />
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{eventType?.requiresConfirmation && (
|
{eventType?.requiresConfirmation && (
|
||||||
@@ -85,11 +87,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
{t("requires_confirmation")}
|
{t("requires_confirmation")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{!isBookingPage && !props.rescheduleUid ? (
|
<AvailableEventLocations
|
||||||
<AvailableEventLocations
|
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
||||||
locations={eventType.locations as AvailabilityPageProps["eventType"]["locations"]}
|
/>
|
||||||
/>
|
|
||||||
) : null}
|
|
||||||
<p
|
<p
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"text-sm font-medium",
|
"text-sm font-medium",
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { BookingStatus } from "@prisma/client";
|
import { BookingStatus } from "@prisma/client";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState, useMemo } from "react";
|
||||||
|
|
||||||
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
import { EventLocationType, getEventLocationType } from "@calcom/app-store/locations";
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryInput, inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
import Badge from "@calcom/ui/v2/core/Badge";
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
import MeetingTimeInTimezones from "@calcom/ui/v2/core/MeetingTimeInTimezones";
|
||||||
|
import Tooltip from "@calcom/ui/v2/core/Tooltip";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
import { extractRecurringDates } from "@lib/parseDate";
|
||||||
|
|
||||||
// import { extractRecurringDates } from "@lib/parseDate";
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
import TableActions, { ActionType } from "@components/ui/TableActions";
|
import TableActions, { ActionType } from "@components/ui/TableActions";
|
||||||
@@ -174,18 +176,20 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
setLocationMutation.mutate({ bookingId: booking.id, newLocation });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Extract recurring dates is intensive to run, so use useMemo.
|
||||||
// Calculate the booking date(s) and setup recurring event data to show
|
// Calculate the booking date(s) and setup recurring event data to show
|
||||||
const recurringStrings: string[] = [];
|
|
||||||
const recurringDates: Date[] = [];
|
|
||||||
|
|
||||||
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
|
// @FIXME: This is importing the RRULE library which is already heavy. Find out a more optimal way do this.
|
||||||
// if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
|
const [recurringStrings, recurringDates] = useMemo(() => {
|
||||||
// [recurringStrings, recurringDates] = extractRecurringDates(booking, user?.timeZone, i18n);
|
if (booking.recurringBookings !== undefined && booking.eventType.recurringEvent?.freq !== undefined) {
|
||||||
// }
|
return extractRecurringDates(booking, user?.timeZone, i18n);
|
||||||
|
}
|
||||||
|
return [[], []];
|
||||||
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [user?.timeZone, i18n.language, booking.recurringBookings]);
|
||||||
|
|
||||||
const location = booking.location || "";
|
const location = booking.location || "";
|
||||||
|
|
||||||
const onClick = () => {
|
const onClickTableData = () => {
|
||||||
router.push({
|
router.push({
|
||||||
pathname: "/success",
|
pathname: "/success",
|
||||||
query: {
|
query: {
|
||||||
@@ -207,8 +211,6 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const utcOffset = dayjs().tz(user?.timeZone).utcOffset();
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -257,19 +259,21 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
<tr className="flex flex-col hover:bg-neutral-50 sm:flex-row">
|
||||||
<td className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[10rem]" onClick={onClick}>
|
<td
|
||||||
|
className="hidden align-top ltr:pl-6 rtl:pr-6 sm:table-cell sm:min-w-[12rem]"
|
||||||
|
onClick={onClickTableData}>
|
||||||
<div className="cursor-pointer py-4">
|
<div className="cursor-pointer py-4">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||||
<div className="text-sm text-gray-500">
|
<div className="text-sm text-gray-500">
|
||||||
{dayjs
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
.utc(booking.startTime)
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
.utcOffset(utcOffset)
|
<MeetingTimeInTimezones
|
||||||
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
|
timeFormat={user?.timeFormat}
|
||||||
-{" "}
|
userTimezone={user?.timeZone}
|
||||||
{dayjs
|
startTime={booking.startTime}
|
||||||
.utc(booking.endTime)
|
endTime={booking.endTime}
|
||||||
.utcOffset(utcOffset)
|
attendees={booking.attendees}
|
||||||
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{isPending && (
|
{isPending && (
|
||||||
@@ -290,29 +294,25 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="mt-2 text-sm text-gray-400">
|
<div className="mt-2 text-sm text-gray-400">
|
||||||
<RecurringBookingsTooltip
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
booking={booking}
|
|
||||||
recurringStrings={recurringStrings}
|
|
||||||
recurringDates={recurringDates}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClick}>
|
<td className={"w-full px-4" + (isRejected ? " line-through" : "")} onClick={onClickTableData}>
|
||||||
{/* Time and Badges for mobile */}
|
{/* Time and Badges for mobile */}
|
||||||
<div className="w-full pt-4 pb-2 sm:hidden">
|
<div className="w-full pt-4 pb-2 sm:hidden">
|
||||||
<div className="flex w-full items-center justify-between sm:hidden">
|
<div className="flex w-full items-center justify-between sm:hidden">
|
||||||
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
<div className="text-sm leading-6 text-gray-900">{startTime}</div>
|
||||||
<div className="pr-2 text-sm text-gray-500">
|
<div className="pr-2 text-sm text-gray-500">
|
||||||
{dayjs
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)} -{" "}
|
||||||
.utc(booking.startTime)
|
{formatTime(booking.endTime, user?.timeFormat, user?.timeZone)}
|
||||||
.utcOffset(utcOffset)
|
<MeetingTimeInTimezones
|
||||||
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}{" "}
|
timeFormat={user?.timeFormat}
|
||||||
-{" "}
|
userTimezone={user?.timeZone}
|
||||||
{dayjs
|
startTime={booking.startTime}
|
||||||
.utc(booking.endTime)
|
endTime={booking.endTime}
|
||||||
.utcOffset(utcOffset)
|
attendees={booking.attendees}
|
||||||
.format(user && user.timeFormat === 12 ? "h:mma" : "HH:mm")}
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -332,11 +332,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
<div className="text-sm text-gray-400 sm:hidden">
|
<div className="text-sm text-gray-400 sm:hidden">
|
||||||
<RecurringBookingsTooltip
|
<RecurringBookingsTooltip booking={booking} recurringDates={recurringDates} />
|
||||||
booking={booking}
|
|
||||||
recurringStrings={recurringStrings}
|
|
||||||
recurringDates={recurringDates}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -356,7 +352,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
</div>
|
</div>
|
||||||
{booking.description && (
|
{booking.description && (
|
||||||
<div
|
<div
|
||||||
className="max-w-52 md:max-w-96 truncate text-sm text-gray-600"
|
className="max-w-10/12 sm:max-w-40 md:max-w-56 xl:max-w-80 lg:max-w-64 truncate text-sm text-gray-600"
|
||||||
title={booking.description}>
|
title={booking.description}>
|
||||||
"{booking.description}"
|
"{booking.description}"
|
||||||
</div>
|
</div>
|
||||||
@@ -397,17 +393,18 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
interface RecurringBookingsTooltipProps {
|
interface RecurringBookingsTooltipProps {
|
||||||
booking: BookingItemProps;
|
booking: BookingItemProps;
|
||||||
recurringStrings: string[];
|
|
||||||
recurringDates: Date[];
|
recurringDates: Date[];
|
||||||
}
|
}
|
||||||
|
|
||||||
const RecurringBookingsTooltip = ({
|
const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookingsTooltipProps) => {
|
||||||
booking,
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
recurringStrings,
|
const query = useMeQuery();
|
||||||
recurringDates,
|
const user = query.data;
|
||||||
}: RecurringBookingsTooltipProps) => {
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const now = new Date();
|
const now = new Date();
|
||||||
|
const recurringCount = recurringDates.filter((date) => {
|
||||||
|
return date >= now;
|
||||||
|
}).length;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
(booking.recurringBookings &&
|
(booking.recurringBookings &&
|
||||||
@@ -418,9 +415,11 @@ const RecurringBookingsTooltip = ({
|
|||||||
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
<div className="underline decoration-gray-400 decoration-dashed underline-offset-2">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={recurringStrings.map((aDate, key) => (
|
content={recurringDates.map((aDate, key) => (
|
||||||
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
|
<p key={key} className={classNames(recurringDates[key] < now && "line-through")}>
|
||||||
{aDate}
|
{formatTime(booking.startTime, user?.timeFormat, user?.timeZone)}
|
||||||
|
{" - "}
|
||||||
|
{dayjs(aDate).format("D MMMM YYYY")}
|
||||||
</p>
|
</p>
|
||||||
))}>
|
))}>
|
||||||
<div className="text-gray-600 dark:text-white">
|
<div className="text-gray-600 dark:text-white">
|
||||||
@@ -431,14 +430,12 @@ const RecurringBookingsTooltip = ({
|
|||||||
<p className="mt-1 pl-5 text-xs">
|
<p className="mt-1 pl-5 text-xs">
|
||||||
{booking.status === BookingStatus.ACCEPTED
|
{booking.status === BookingStatus.ACCEPTED
|
||||||
? `${t("event_remaining", {
|
? `${t("event_remaining", {
|
||||||
count: recurringDates.length,
|
count: recurringCount,
|
||||||
})}`
|
})}`
|
||||||
: getEveryFreqFor({
|
: getEveryFreqFor({
|
||||||
t,
|
t,
|
||||||
recurringEvent: booking.eventType.recurringEvent,
|
recurringEvent: booking.eventType.recurringEvent,
|
||||||
recurringCount: recurringDates.filter((date) => {
|
recurringCount,
|
||||||
return date >= now;
|
|
||||||
}).length,
|
|
||||||
})}
|
})}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type Props = {
|
|||||||
booking: {
|
booking: {
|
||||||
title?: string;
|
title?: string;
|
||||||
uid?: string;
|
uid?: string;
|
||||||
|
id?: number;
|
||||||
};
|
};
|
||||||
profile: {
|
profile: {
|
||||||
name: string | null;
|
name: string | null;
|
||||||
@@ -57,13 +58,13 @@ export default function CancelBooking(props: Props) {
|
|||||||
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
className="mt-2 mb-3 w-full dark:border-gray-900 dark:bg-gray-700 dark:text-white sm:mb-3 "
|
||||||
rows={3}
|
rows={3}
|
||||||
/>
|
/>
|
||||||
<div className=" flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
|
<div className="flex flex-col-reverse rtl:space-x-reverse sm:flex-row">
|
||||||
{!props.recurringEvent && (
|
{!props.recurringEvent && (
|
||||||
<div className=" border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
|
<div className="border-bookinglightest mt-5 flex w-full justify-center border-t pt-3 sm:mt-0 sm:justify-start sm:border-0 sm:pt-0">
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="border-0 sm:border"
|
className="border-0 sm:border"
|
||||||
onClick={() => router.push("/reschedule/" + booking?.uid)}>
|
onClick={() => router.push(`/reschedule/${booking?.uid}`)}>
|
||||||
{t("reschedule_this")}
|
{t("reschedule_this")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -79,8 +80,8 @@ export default function CancelBooking(props: Props) {
|
|||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
uid: booking?.uid,
|
id: booking?.id,
|
||||||
reason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
};
|
};
|
||||||
|
|
||||||
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
telemetry.event(telemetryEventTypes.bookingCancelled, collectPageParameters());
|
||||||
|
|||||||
@@ -1,25 +1,19 @@
|
|||||||
import { FC, useEffect, useState } from "react";
|
import { FC, useEffect, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Switch } from "@calcom/ui/v2";
|
|
||||||
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect";
|
import TimezoneSelect, { ITimezoneOption } from "@calcom/ui/v2/core/TimezoneSelect";
|
||||||
|
|
||||||
import { is24h, timeZone } from "../../lib/clock";
|
import { timeZone } from "../../lib/clock";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
onSelectTimeZone: (selectedTimeZone: string) => void;
|
onSelectTimeZone: (selectedTimeZone: string) => void;
|
||||||
onToggle24hClock: (is24hClock: boolean) => void;
|
|
||||||
timeFormat: string;
|
|
||||||
hideTimeFormatToggle?: boolean;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat, hideTimeFormatToggle }) => {
|
const TimeOptions: FC<Props> = ({ onSelectTimeZone }) => {
|
||||||
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
const [selectedTimeZone, setSelectedTimeZone] = useState("");
|
||||||
const [is24hClock, setIs24hClock] = useState(timeFormat === "HH:mm" && true);
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIs24hClock(is24h());
|
|
||||||
setSelectedTimeZone(timeZone());
|
setSelectedTimeZone(timeZone());
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -29,28 +23,10 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
|
|||||||
}
|
}
|
||||||
}, [selectedTimeZone, onSelectTimeZone]);
|
}, [selectedTimeZone, onSelectTimeZone]);
|
||||||
|
|
||||||
const handle24hClockToggle = (is24hClock: boolean) => {
|
|
||||||
setIs24hClock(is24hClock);
|
|
||||||
onToggle24hClock(is24h(is24hClock));
|
|
||||||
};
|
|
||||||
|
|
||||||
return selectedTimeZone !== "" ? (
|
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="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="mb-4 flex">
|
||||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||||
{!hideTimeFormatToggle && (
|
|
||||||
<div className="ml-auto flex items-center">
|
|
||||||
<label className="ltl:mr-3 mr-2 align-text-top text-sm font-medium text-neutral-700 ltr:ml-3 rtl:mr-3 dark:text-white">
|
|
||||||
{t("am_pm")}
|
|
||||||
</label>
|
|
||||||
<Switch
|
|
||||||
name="24hClock"
|
|
||||||
label={t("24_h")}
|
|
||||||
defaultChecked={is24hClock}
|
|
||||||
onCheckedChange={handle24hClockToggle}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
<TimezoneSelect
|
<TimezoneSelect
|
||||||
id="timeZone"
|
id="timeZone"
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
// Get router variables
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
|
||||||
import { EventType } from "@prisma/client";
|
import { EventType } from "@prisma/client";
|
||||||
import * as Popover from "@radix-ui/react-popover";
|
import * as Popover from "@radix-ui/react-popover";
|
||||||
import { TFunction } from "next-i18next";
|
import { TFunction } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useReducer, useEffect, useMemo, useState, useRef } from "react";
|
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -27,7 +26,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
|||||||
import notEmpty from "@calcom/lib/notEmpty";
|
import notEmpty from "@calcom/lib/notEmpty";
|
||||||
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { detectBrowserTimeFormat, getIs24hClockFromLocalStorage } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
import DatePicker from "@calcom/ui/v2/modules/booker/DatePicker";
|
||||||
@@ -48,6 +47,7 @@ import type { AvailabilityPageProps } from "../../../pages/[user]/[type]";
|
|||||||
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
import type { DynamicAvailabilityPageProps } from "../../../pages/d/[link]/[slug]";
|
||||||
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
import type { AvailabilityTeamPageProps } from "../../../pages/team/[slug]/[type]";
|
||||||
|
|
||||||
|
// Get router variables
|
||||||
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
const GoBackToPreviousPage = ({ t }: { t: TFunction }) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const path = router.asPath.split("/");
|
const path = router.asPath.split("/");
|
||||||
@@ -108,6 +108,7 @@ const useSlots = ({
|
|||||||
const SlotPicker = ({
|
const SlotPicker = ({
|
||||||
eventType,
|
eventType,
|
||||||
timeFormat,
|
timeFormat,
|
||||||
|
onTimeFormatChange,
|
||||||
timeZone,
|
timeZone,
|
||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
users,
|
users,
|
||||||
@@ -116,7 +117,8 @@ const SlotPicker = ({
|
|||||||
ethSignature,
|
ethSignature,
|
||||||
}: {
|
}: {
|
||||||
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
eventType: Pick<EventType, "id" | "schedulingType" | "slug">;
|
||||||
timeFormat: string;
|
timeFormat: TimeFormat;
|
||||||
|
onTimeFormatChange: (is24Hour: boolean) => void;
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
seatsPerTimeSlot?: number;
|
seatsPerTimeSlot?: number;
|
||||||
recurringEventCount?: number;
|
recurringEventCount?: number;
|
||||||
@@ -129,11 +131,8 @@ const SlotPicker = ({
|
|||||||
const { date, setQuery: setDate } = useRouterQuery("date");
|
const { date, setQuery: setDate } = useRouterQuery("date");
|
||||||
const { month, setQuery: setMonth } = useRouterQuery("month");
|
const { month, setQuery: setMonth } = useRouterQuery("month");
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const slotPickerRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
slotPickerRef.current && autoAnimate(slotPickerRef.current);
|
|
||||||
}, [slotPickerRef]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!router.isReady) return;
|
if (!router.isReady) return;
|
||||||
@@ -203,6 +202,7 @@ const SlotPicker = ({
|
|||||||
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
|
slots={selectedDate && slots[selectedDate.format("YYYY-MM-DD")]}
|
||||||
date={selectedDate}
|
date={selectedDate}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
onTimeFormatChange={onTimeFormatChange}
|
||||||
eventTypeId={eventType.id}
|
eventTypeId={eventType.id}
|
||||||
eventTypeSlug={eventType.slug}
|
eventTypeSlug={eventType.slug}
|
||||||
seatsPerTimeSlot={seatsPerTimeSlot}
|
seatsPerTimeSlot={seatsPerTimeSlot}
|
||||||
@@ -215,35 +215,20 @@ const SlotPicker = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
function TimezoneDropdown({
|
function TimezoneDropdown({
|
||||||
onChangeTimeFormat,
|
|
||||||
onChangeTimeZone,
|
onChangeTimeZone,
|
||||||
timeZone,
|
timeZone,
|
||||||
timeFormat,
|
|
||||||
hideTimeFormatToggle,
|
|
||||||
}: {
|
}: {
|
||||||
onChangeTimeFormat: (newTimeFormat: string) => void;
|
|
||||||
onChangeTimeZone: (newTimeZone: string) => void;
|
onChangeTimeZone: (newTimeZone: string) => void;
|
||||||
timeZone?: string;
|
timeZone?: string;
|
||||||
timeFormat: string;
|
|
||||||
hideTimeFormatToggle?: boolean;
|
|
||||||
}) {
|
}) {
|
||||||
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
const [isTimeOptionsOpen, setIsTimeOptionsOpen] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
handleToggle24hClock(!!getIs24hClockFromLocalStorage());
|
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleSelectTimeZone = (newTimeZone: string) => {
|
const handleSelectTimeZone = (newTimeZone: string) => {
|
||||||
onChangeTimeZone(newTimeZone);
|
onChangeTimeZone(newTimeZone);
|
||||||
localStorageTimeZone(newTimeZone);
|
localStorageTimeZone(newTimeZone);
|
||||||
setIsTimeOptionsOpen(false);
|
setIsTimeOptionsOpen(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
const handleToggle24hClock = (is24hClock: boolean) => {
|
|
||||||
onChangeTimeFormat(is24hClock ? "HH:mm" : "h:mma");
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
<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 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
<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 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
||||||
@@ -262,12 +247,7 @@ function TimezoneDropdown({
|
|||||||
hideWhenDetached
|
hideWhenDetached
|
||||||
align="start"
|
align="start"
|
||||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
||||||
<TimeOptions
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
onSelectTimeZone={handleSelectTimeZone}
|
|
||||||
onToggle24hClock={handleToggle24hClock}
|
|
||||||
timeFormat={timeFormat}
|
|
||||||
hideTimeFormatToggle={hideTimeFormatToggle}
|
|
||||||
/>
|
|
||||||
</Popover.Content>
|
</Popover.Content>
|
||||||
</Popover.Portal>
|
</Popover.Portal>
|
||||||
</Popover.Root>
|
</Popover.Root>
|
||||||
@@ -299,16 +279,9 @@ const useRouterQuery = <T extends string>(name: T) => {
|
|||||||
|
|
||||||
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
export type Props = AvailabilityTeamPageProps | AvailabilityPageProps | DynamicAvailabilityPageProps;
|
||||||
|
|
||||||
const timeFormatTotimeFormatString = (timeFormat?: number | null) => {
|
const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||||
if (!timeFormat) return null;
|
|
||||||
return timeFormat === 24 ? "HH:mm" : "h:mma";
|
|
||||||
};
|
|
||||||
|
|
||||||
const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|
||||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
|
||||||
const timeFormatFromProfile = timeFormatTotimeFormatString(user?.timeFormat);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const query = dateQuerySchema.parse(router.query);
|
const query = dateQuerySchema.parse(router.query);
|
||||||
const { rescheduleUid } = query;
|
const { rescheduleUid } = query;
|
||||||
useTheme(profile.theme);
|
useTheme(profile.theme);
|
||||||
@@ -319,7 +292,12 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
const isBackgroundTransparent = useIsBackgroundTransparent();
|
const isBackgroundTransparent = useIsBackgroundTransparent();
|
||||||
|
|
||||||
const [timeZone, setTimeZone] = useState<string>();
|
const [timeZone, setTimeZone] = useState<string>();
|
||||||
const [timeFormat, setTimeFormat] = useState<string>("HH:mm");
|
const [timeFormat, setTimeFormat] = useState<TimeFormat>(detectBrowserTimeFormat);
|
||||||
|
|
||||||
|
const onTimeFormatChange = (is24Hours: boolean) => {
|
||||||
|
setTimeFormat(is24Hours ? TimeFormat.TWENTY_FOUR_HOUR : TimeFormat.TWELVE_HOUR);
|
||||||
|
setIs24hClockInLocalStorage(is24Hours);
|
||||||
|
};
|
||||||
|
|
||||||
const [gateState, gateDispatcher] = useReducer(
|
const [gateState, gateDispatcher] = useReducer(
|
||||||
(state: GateState, newState: Partial<GateState>) => ({
|
(state: GateState, newState: Partial<GateState>) => ({
|
||||||
@@ -331,8 +309,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
setTimeZone(localStorageTimeZone() || dayjs.tz.guess());
|
||||||
setTimeFormat(timeFormatFromProfile || detectBrowserTimeFormat);
|
}, []);
|
||||||
}, [timeFormatFromProfile]);
|
|
||||||
|
|
||||||
// TODO: Improve this;
|
// TODO: Improve this;
|
||||||
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
useExposePlanGlobally(eventType.users.length === 1 ? eventType.users[0].plan : "PRO");
|
||||||
@@ -354,24 +331,13 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
|
|
||||||
const timezoneDropdown = useMemo(
|
const timezoneDropdown = useMemo(
|
||||||
() => (
|
() => <TimezoneDropdown timeZone={timeZone} onChangeTimeZone={setTimeZone} />,
|
||||||
<TimezoneDropdown
|
[timeZone]
|
||||||
timeFormat={timeFormat}
|
|
||||||
onChangeTimeFormat={setTimeFormat}
|
|
||||||
timeZone={timeZone}
|
|
||||||
onChangeTimeZone={setTimeZone}
|
|
||||||
// Currently we don't allow the user to change the timeformat when they're logged in,
|
|
||||||
// the only way to change it is if they go to their profile.
|
|
||||||
hideTimeFormatToggle={!!timeFormatFromProfile}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
[timeZone, timeFormat, timeFormatFromProfile]
|
|
||||||
);
|
);
|
||||||
const stripeAppData = getStripeAppData(eventType);
|
const stripeAppData = getStripeAppData(eventType);
|
||||||
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
const rainbowAppData = getEventTypeAppData(eventType, "rainbow") || {};
|
||||||
const rawSlug = profile.slug ? profile.slug.split("/") : [];
|
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.
|
if (rawSlug.length > 1) rawSlug.pop(); //team events have team name as slug, but user events have [user]/[type] as slug.
|
||||||
const slug = rawSlug.join("/");
|
|
||||||
|
|
||||||
// Define conditional gates here
|
// Define conditional gates here
|
||||||
const gates = [
|
const gates = [
|
||||||
@@ -386,8 +352,16 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
<HeadSeo
|
<HeadSeo
|
||||||
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
title={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title} | ${profile.name}`}
|
||||||
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
description={`${rescheduleUid ? t("reschedule") : ""} ${eventType.title}`}
|
||||||
name={profile.name || undefined}
|
meeting={{
|
||||||
username={slug || undefined}
|
title: eventType.title,
|
||||||
|
profile: { name: `${profile.name}`, image: profile.image },
|
||||||
|
users: [
|
||||||
|
...(eventType.users || []).map((user) => ({
|
||||||
|
name: `${user.name}`,
|
||||||
|
username: `${user.username}`,
|
||||||
|
})),
|
||||||
|
],
|
||||||
|
}}
|
||||||
nextSeoProps={{
|
nextSeoProps={{
|
||||||
nofollow: eventType.hidden,
|
nofollow: eventType.hidden,
|
||||||
noindex: eventType.hidden,
|
noindex: eventType.hidden,
|
||||||
@@ -416,7 +390,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
"sm:dark:border-darkgray-200 flex flex-col border-gray-200 p-5 sm:border-r",
|
||||||
"min-w-full md:w-[280px] md:min-w-[280px]",
|
"min-w-full md:w-[230px] md:min-w-[230px]",
|
||||||
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
recurringEventCount && "xl:w-[380px] xl:min-w-[380px]"
|
||||||
)}>
|
)}>
|
||||||
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
<BookingDescription profile={profile} eventType={eventType} rescheduleUid={rescheduleUid}>
|
||||||
@@ -496,6 +470,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||||||
}
|
}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
timeFormat={timeFormat}
|
timeFormat={timeFormat}
|
||||||
|
onTimeFormatChange={onTimeFormatChange}
|
||||||
timeZone={timeZone}
|
timeZone={timeZone}
|
||||||
users={userList}
|
users={userList}
|
||||||
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
seatsPerTimeSlot={eventType.seatsPerTimeSlot || undefined}
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
import { EventTypeCustomInputType, WorkflowActions } from "@prisma/client";
|
||||||
import { SchedulingType } from "@prisma/client";
|
|
||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
@@ -32,6 +31,7 @@ import {
|
|||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { formatTime } from "@calcom/lib/date-fns";
|
||||||
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
import getStripeAppData from "@calcom/lib/getStripeAppData";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
@@ -47,6 +47,7 @@ import { Button } from "@calcom/ui/v2";
|
|||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import { ensureArray } from "@lib/ensureArray";
|
import { ensureArray } from "@lib/ensureArray";
|
||||||
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
import createBooking from "@lib/mutations/bookings/create-booking";
|
import createBooking from "@lib/mutations/bookings/create-booking";
|
||||||
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
import createRecurringBooking from "@lib/mutations/bookings/create-recurring-booking";
|
||||||
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
import { parseDate, parseRecurringDates } from "@lib/parseDate";
|
||||||
@@ -84,9 +85,13 @@ const BookingPage = ({
|
|||||||
recurringEventCount,
|
recurringEventCount,
|
||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const isEmbed = useIsEmbed();
|
// Get user so we can determine 12/24 hour format preferences
|
||||||
|
const query = useMeQuery();
|
||||||
|
const user = query.data;
|
||||||
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -115,14 +120,14 @@ const BookingPage = ({
|
|||||||
|
|
||||||
const mutation = useMutation(createBooking, {
|
const mutation = useMutation(createBooking, {
|
||||||
onSuccess: async (responseData) => {
|
onSuccess: async (responseData) => {
|
||||||
const { id, attendees, paymentUid } = responseData;
|
const { id, paymentUid } = responseData;
|
||||||
if (paymentUid) {
|
if (paymentUid) {
|
||||||
return await router.push(
|
return await router.push(
|
||||||
createPaymentLink({
|
createPaymentLink({
|
||||||
paymentUid,
|
paymentUid,
|
||||||
date,
|
date,
|
||||||
name: attendees[0].name,
|
name: bookingForm.getValues("name"),
|
||||||
email: attendees[0].email,
|
email: bookingForm.getValues("email"),
|
||||||
absolute: false,
|
absolute: false,
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
@@ -136,8 +141,8 @@ const BookingPage = ({
|
|||||||
eventSlug: eventType.slug,
|
eventSlug: eventType.slug,
|
||||||
username: profile.slug,
|
username: profile.slug,
|
||||||
reschedule: !!rescheduleUid,
|
reschedule: !!rescheduleUid,
|
||||||
name: attendees[0].name,
|
name: bookingForm.getValues("name"),
|
||||||
email: attendees[0].email,
|
email: bookingForm.getValues("email"),
|
||||||
location: responseData.location,
|
location: responseData.location,
|
||||||
eventName: profile.eventName || "",
|
eventName: profile.eventName || "",
|
||||||
bookingId: id,
|
bookingId: id,
|
||||||
@@ -262,11 +267,13 @@ const BookingPage = ({
|
|||||||
phone: z
|
phone: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
.optional(),
|
.optional()
|
||||||
|
.nullable(),
|
||||||
smsReminderNumber: z
|
smsReminderNumber: z
|
||||||
.string()
|
.string()
|
||||||
.refine((val) => isValidPhoneNumber(val))
|
.refine((val) => isValidPhoneNumber(val))
|
||||||
.optional(),
|
.optional()
|
||||||
|
.nullable(),
|
||||||
})
|
})
|
||||||
.passthrough();
|
.passthrough();
|
||||||
|
|
||||||
@@ -358,7 +365,9 @@ const BookingPage = ({
|
|||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
selectedLocationType === LocationType.Phone
|
||||||
|
? booking.phone
|
||||||
|
: booking.smsReminderNumber || undefined,
|
||||||
ethSignature: gateState.rainbowToken,
|
ethSignature: gateState.rainbowToken,
|
||||||
}));
|
}));
|
||||||
recurringMutation.mutate(recurringBookings);
|
recurringMutation.mutate(recurringBookings);
|
||||||
@@ -386,7 +395,9 @@ const BookingPage = ({
|
|||||||
hasHashedBookingLink,
|
hasHashedBookingLink,
|
||||||
hashedLink,
|
hashedLink,
|
||||||
smsReminderNumber:
|
smsReminderNumber:
|
||||||
selectedLocationType === LocationType.Phone ? booking.phone : booking.smsReminderNumber,
|
selectedLocationType === LocationType.Phone
|
||||||
|
? booking.phone
|
||||||
|
: booking.smsReminderNumber || undefined,
|
||||||
ethSignature: gateState.rainbowToken,
|
ethSignature: gateState.rainbowToken,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -400,6 +411,7 @@ const BookingPage = ({
|
|||||||
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
|
"dark:placeholder:text-darkgray-600 focus:border-brand dark:border-darkgray-300 dark:text-darkgray-900 block w-full rounded-md border-gray-300 text-sm focus:ring-black disabled:bg-gray-200 disabled:hover:cursor-not-allowed dark:bg-transparent dark:selection:bg-green-500 disabled:dark:text-gray-500";
|
||||||
|
|
||||||
let isSmsReminderNumberNeeded = false;
|
let isSmsReminderNumberNeeded = false;
|
||||||
|
let isSmsReminderNumberRequired = false;
|
||||||
|
|
||||||
if (eventType.workflows.length > 0) {
|
if (eventType.workflows.length > 0) {
|
||||||
eventType.workflows.forEach((workflowReference) => {
|
eventType.workflows.forEach((workflowReference) => {
|
||||||
@@ -407,6 +419,7 @@ const BookingPage = ({
|
|||||||
workflowReference.workflow.steps.forEach((step) => {
|
workflowReference.workflow.steps.forEach((step) => {
|
||||||
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
if (step.action === WorkflowActions.SMS_ATTENDEE) {
|
||||||
isSmsReminderNumberNeeded = true;
|
isSmsReminderNumberNeeded = true;
|
||||||
|
isSmsReminderNumberRequired = step.numberRequired || false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -485,15 +498,25 @@ const BookingPage = ({
|
|||||||
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiCalendar className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
{(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
parseDate(dayjs(date).tz(timeZone()), i18n)}
|
`${formatTime(dayjs(date).toDate(), user?.timeFormat, user?.timeZone)}, ${dayjs(
|
||||||
|
date
|
||||||
|
).format("dddd, D MMMM YYYY")}`}
|
||||||
{!rescheduleUid &&
|
{!rescheduleUid &&
|
||||||
eventType.recurringEvent?.freq &&
|
eventType.recurringEvent?.freq &&
|
||||||
recurringStrings.slice(0, 5).map((aDate, key) => <p key={key}>{aDate}</p>)}
|
recurringDates.slice(0, 5).map((aDate, key) => {
|
||||||
|
return (
|
||||||
|
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
|
||||||
|
aDate
|
||||||
|
).format("dddd, D MMMM YYYY")}`}</p>
|
||||||
|
);
|
||||||
|
})}
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringStrings.length > 5 && (
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<Tooltip
|
<Tooltip
|
||||||
content={recurringStrings.slice(5).map((aDate, key) => (
|
content={recurringDates.slice(5).map((aDate, key) => (
|
||||||
<p key={key}>{aDate}</p>
|
<p key={key}>{`${formatTime(aDate, user?.timeFormat, user?.timeZone)}, ${dayjs(
|
||||||
|
aDate
|
||||||
|
).format("dddd, D MMMM YYYY")}`}</p>
|
||||||
))}>
|
))}>
|
||||||
<p className="dark:text-darkgray-600 text-sm">
|
<p className="dark:text-darkgray-600 text-sm">
|
||||||
{t("plus_more", { count: recurringStrings.length - 5 })}
|
{t("plus_more", { count: recurringStrings.length - 5 })}
|
||||||
@@ -516,7 +539,15 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<Icon.FiUser className="mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4" />
|
<Icon.FiUser
|
||||||
|
className={`mr-[10px] ml-[2px] mt-[2px] inline-block h-4 w-4 ${
|
||||||
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
|
? "text-rose-600"
|
||||||
|
: booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.33
|
||||||
|
? "text-yellow-500"
|
||||||
|
: "text-bookinghighlight"
|
||||||
|
}`}
|
||||||
|
/>
|
||||||
<p
|
<p
|
||||||
className={`${
|
className={`${
|
||||||
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
booking && booking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
@@ -577,48 +608,51 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="mb-4">
|
<>
|
||||||
<>
|
{rescheduleUid ? (
|
||||||
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
<div className="mb-4">
|
||||||
{t("location")}
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
</span>
|
{t("location")}
|
||||||
{rescheduleUid ? (
|
</span>
|
||||||
<p className="mt-1 text-sm text-gray-500">
|
<p className="mt-1 text-sm text-gray-500">
|
||||||
{getHumanReadableLocationValue(booking?.location, t)}
|
{getHumanReadableLocationValue(booking?.location, t)}
|
||||||
</p>
|
</p>
|
||||||
) : (
|
</div>
|
||||||
locations.length > 1 && (
|
) : (
|
||||||
<>
|
locations.length > 1 && (
|
||||||
{locations.map((location, i) => {
|
<div className="mb-4">
|
||||||
const locationString = locationKeyToString(location);
|
<span className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
if (!selectedLocationType) {
|
{t("location")}
|
||||||
bookingForm.setValue("locationType", locations[0].type);
|
</span>
|
||||||
}
|
{locations.map((location, i) => {
|
||||||
if (typeof locationString !== "string") {
|
const locationString = locationKeyToString(location);
|
||||||
// It's possible that location app got uninstalled
|
if (!selectedLocationType) {
|
||||||
return null;
|
bookingForm.setValue("locationType", locations[0].type);
|
||||||
}
|
}
|
||||||
return (
|
if (typeof locationString !== "string") {
|
||||||
<label key={i} className="block">
|
// It's possible that location app got uninstalled
|
||||||
<input
|
return null;
|
||||||
type="radio"
|
}
|
||||||
disabled={!!disableLocations}
|
return (
|
||||||
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
<label key={i} className="block">
|
||||||
{...bookingForm.register("locationType", { required: true })}
|
<input
|
||||||
value={location.type}
|
type="radio"
|
||||||
defaultChecked={i === 0}
|
disabled={!!disableLocations}
|
||||||
/>
|
className="location dark:bg-darkgray-300 dark:border-darkgray-300 h-4 w-4 border-gray-300 text-black focus:ring-black ltr:mr-2 rtl:ml-2"
|
||||||
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
|
{...bookingForm.register("locationType", { required: true })}
|
||||||
{locationKeyToString(location)}
|
value={location.type}
|
||||||
</span>
|
defaultChecked={i === 0}
|
||||||
</label>
|
/>
|
||||||
);
|
<span className="text-sm ltr:ml-2 rtl:mr-2 dark:text-white">
|
||||||
})}
|
{locationKeyToString(location)}
|
||||||
</>
|
</span>
|
||||||
)
|
</label>
|
||||||
)}
|
);
|
||||||
</>
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
</>
|
||||||
{/* TODO: Change name and id ="phone" to something generic */}
|
{/* TODO: Change name and id ="phone" to something generic */}
|
||||||
{AttendeeInput && (
|
{AttendeeInput && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
@@ -778,7 +812,7 @@ const BookingPage = ({
|
|||||||
<label
|
<label
|
||||||
htmlFor="smsReminderNumber"
|
htmlFor="smsReminderNumber"
|
||||||
className="block text-sm font-medium text-gray-700 dark:text-white">
|
className="block text-sm font-medium text-gray-700 dark:text-white">
|
||||||
{t("number_for_sms_reminders")}
|
{t("number_sms_notifications")}
|
||||||
</label>
|
</label>
|
||||||
<div className="mt-1">
|
<div className="mt-1">
|
||||||
<PhoneInput<BookingFormValues>
|
<PhoneInput<BookingFormValues>
|
||||||
@@ -786,7 +820,7 @@ const BookingPage = ({
|
|||||||
name="smsReminderNumber"
|
name="smsReminderNumber"
|
||||||
placeholder={t("enter_phone_number")}
|
placeholder={t("enter_phone_number")}
|
||||||
id="smsReminderNumber"
|
id="smsReminderNumber"
|
||||||
required
|
required={isSmsReminderNumberRequired}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
{bookingForm.formState.errors.smsReminderNumber && (
|
{bookingForm.formState.errors.smsReminderNumber && (
|
||||||
|
|||||||
@@ -1,16 +1,12 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
|
||||||
import { RescheduleResponse } from "pages/api/book/request-reschedule";
|
|
||||||
import React, { useState, Dispatch, SetStateAction } from "react";
|
import React, { useState, Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
import { Dialog, DialogClose, DialogContent, DialogFooter, DialogHeader } from "@calcom/ui/Dialog";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { TextArea } from "@calcom/ui/form/fields";
|
import { TextArea } from "@calcom/ui/form/fields";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
import * as fetchWrapper from "@lib/core/http/fetch-wrapper";
|
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -23,35 +19,18 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
|
||||||
const rescheduleApi = useMutation(
|
|
||||||
async () => {
|
|
||||||
setIsLoading(true);
|
|
||||||
try {
|
|
||||||
const result = await fetchWrapper.post<
|
|
||||||
{ bookingId: string; rescheduleReason: string },
|
|
||||||
RescheduleResponse
|
|
||||||
>("/api/book/request-reschedule", {
|
|
||||||
bookingId,
|
|
||||||
rescheduleReason,
|
|
||||||
});
|
|
||||||
|
|
||||||
if (result) {
|
const { mutate: rescheduleApi, isLoading } = trpc.useMutation("viewer.bookings.requestReschedule", {
|
||||||
showToast(t("reschedule_request_sent"), "success");
|
async onSuccess() {
|
||||||
setIsOpenDialog(false);
|
showToast(t("reschedule_request_sent"), "success");
|
||||||
}
|
setIsOpenDialog(false);
|
||||||
} catch (error) {
|
await utils.invalidateQueries(["viewer.bookings"]);
|
||||||
showToast(t("unexpected_error_try_again"), "error");
|
|
||||||
// @TODO: notify sentry
|
|
||||||
}
|
|
||||||
setIsLoading(false);
|
|
||||||
},
|
},
|
||||||
{
|
onError() {
|
||||||
async onSettled() {
|
showToast(t("unexpected_error_try_again"), "error");
|
||||||
await utils.invalidateQueries(["viewer.bookings"]);
|
// @TODO: notify sentry
|
||||||
},
|
},
|
||||||
}
|
});
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
@@ -84,7 +63,10 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
rescheduleApi.mutate();
|
rescheduleApi({
|
||||||
|
bookingId,
|
||||||
|
rescheduleReason,
|
||||||
|
});
|
||||||
}}>
|
}}>
|
||||||
{t("send_reschedule_request")}
|
{t("send_reschedule_request")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import type { z } from "zod";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
import { createEventTypeInput } from "@calcom/prisma/zod/custom/eventtype";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
@@ -24,6 +23,7 @@ import Dropdown, {
|
|||||||
} from "@calcom/ui/Dropdown";
|
} from "@calcom/ui/Dropdown";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
import { Form, InputLeading, TextAreaField, TextField } from "@calcom/ui/form/fields";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
import { slugify } from "@lib/slugify";
|
import { slugify } from "@lib/slugify";
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Switch } from "@calcom/ui/v2";
|
import { Switch } from "@calcom/ui/v2";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
|
|||||||
@@ -22,12 +22,12 @@ const Steps = (props: ISteps) => {
|
|||||||
key={`step-${index}`}
|
key={`step-${index}`}
|
||||||
onClick={() => navigateToStep(index)}
|
onClick={() => navigateToStep(index)}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"h-1 w-1/4 rounded-[1px] bg-black dark:bg-white",
|
"h-1 w-full rounded-[1px] bg-black dark:bg-white",
|
||||||
index < currentStep ? "cursor-pointer" : ""
|
index < currentStep ? "cursor-pointer" : ""
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div key={`step-${index}`} className="h-1 w-1/4 rounded-[1px] bg-black bg-opacity-25" />
|
<div key={`step-${index}`} className="h-1 w-full rounded-[1px] bg-black bg-opacity-25" />
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import { Fragment } from "react";
|
|||||||
|
|
||||||
import { InstallAppButton } from "@calcom/app-store/components";
|
import { InstallAppButton } from "@calcom/app-store/components";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
@@ -11,6 +10,7 @@ import { List } from "@calcom/ui/List";
|
|||||||
import { ShellSubHeading } from "@calcom/ui/Shell";
|
import { ShellSubHeading } from "@calcom/ui/Shell";
|
||||||
import Switch from "@calcom/ui/Switch";
|
import Switch from "@calcom/ui/Switch";
|
||||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
||||||
import { Dialog } from "@calcom/ui/Dialog";
|
import { Dialog } from "@calcom/ui/Dialog";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
export default function DisconnectIntegration(props: {
|
export default function DisconnectIntegration(props: {
|
||||||
/** Integration credential id */
|
/** Integration credential id */
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { useMutation } from "@tanstack/react-query";
|
import { useMutation } from "@tanstack/react-query";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { ButtonBaseProps } from "@calcom/ui/Button";
|
import { ButtonBaseProps } from "@calcom/ui/Button";
|
||||||
import { Dialog } from "@calcom/ui/Dialog";
|
import { Dialog } from "@calcom/ui/Dialog";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
|
import DeleteStripeDialogContent from "@components/dialog/DeleteStripeDialogContent";
|
||||||
|
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import React, { SyntheticEvent, useState } from "react";
|
|||||||
|
|
||||||
import { ErrorCode } from "@calcom/lib/auth";
|
import { ErrorCode } from "@calcom/lib/auth";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
const ChangePasswordSection = () => {
|
const ChangePasswordSection = () => {
|
||||||
const [oldPassword, setOldPassword] = useState("");
|
const [oldPassword, setOldPassword] = useState("");
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import Badge from "@calcom/ui/Badge";
|
import Badge from "@calcom/ui/Badge";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
const DisableUserImpersonation = ({ disableImpersonation }: { disableImpersonation: boolean }) => {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|||||||
@@ -1,6 +1,14 @@
|
|||||||
import merge from "lodash/merge";
|
import merge from "lodash/merge";
|
||||||
import { NextSeo, NextSeoProps } from "next-seo";
|
import { NextSeo, NextSeoProps } from "next-seo";
|
||||||
import React from "react";
|
|
||||||
|
import {
|
||||||
|
AppImageProps,
|
||||||
|
constructAppImage,
|
||||||
|
constructGenericImage,
|
||||||
|
constructMeetingImage,
|
||||||
|
MeetingImageProps,
|
||||||
|
} from "@calcom/lib/OgImages";
|
||||||
|
import { truncate, truncateOnWord } from "@calcom/lib/text";
|
||||||
|
|
||||||
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
import { getSeoImage, seoConfig } from "@lib/config/next-seo.config";
|
||||||
import { getBrowserInfo } from "@lib/core/browser/browser.utils";
|
import { getBrowserInfo } from "@lib/core/browser/browser.utils";
|
||||||
@@ -9,11 +17,11 @@ export type HeadSeoProps = {
|
|||||||
title: string;
|
title: string;
|
||||||
description: string;
|
description: string;
|
||||||
siteName?: string;
|
siteName?: string;
|
||||||
name?: string;
|
|
||||||
url?: string;
|
url?: string;
|
||||||
username?: string;
|
|
||||||
canonical?: string;
|
canonical?: string;
|
||||||
nextSeoProps?: NextSeoProps;
|
nextSeoProps?: NextSeoProps;
|
||||||
|
app?: AppImageProps;
|
||||||
|
meeting?: MeetingImageProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -63,32 +71,14 @@ const buildSeoMeta = (pageProps: {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const constructImage = (name: string, description: string, username: string): string => {
|
|
||||||
return (
|
|
||||||
encodeURIComponent("Meet **" + name + "** <br>" + description).replace(/'/g, "%27") +
|
|
||||||
".png?md=1&images=https%3A%2F%2Fcal.com%2Flogo-white.svg&images=" +
|
|
||||||
(process.env.NEXT_PUBLIC_WEBSITE_URL || process.env.NEXT_PUBLIC_WEBAPP_URL) +
|
|
||||||
"/" +
|
|
||||||
username +
|
|
||||||
"/avatar.png"
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
||||||
const defaultUrl = getBrowserInfo()?.url;
|
const defaultUrl = getBrowserInfo()?.url;
|
||||||
const image = getSeoImage("default");
|
|
||||||
|
|
||||||
const {
|
const { title, description, siteName, canonical = defaultUrl, nextSeoProps = {}, app, meeting } = props;
|
||||||
title,
|
|
||||||
description,
|
const image = getSeoImage("ogImage") + constructGenericImage({ title, description });
|
||||||
name = null,
|
const truncatedDescription = truncateOnWord(description, 158);
|
||||||
username = null,
|
|
||||||
siteName,
|
|
||||||
canonical = defaultUrl,
|
|
||||||
nextSeoProps = {},
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const truncatedDescription = description.length > 24 ? description.substring(0, 23) + "..." : description;
|
|
||||||
const pageTitle = title + " | Cal.com";
|
const pageTitle = title + " | Cal.com";
|
||||||
let seoObject = buildSeoMeta({
|
let seoObject = buildSeoMeta({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
@@ -98,8 +88,20 @@ export const HeadSeo = (props: HeadSeoProps): JSX.Element => {
|
|||||||
siteName,
|
siteName,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (name && username) {
|
if (meeting) {
|
||||||
const pageImage = getSeoImage("ogImage") + constructImage(name, truncatedDescription, username);
|
const pageImage = getSeoImage("ogImage") + constructMeetingImage(meeting);
|
||||||
|
seoObject = buildSeoMeta({
|
||||||
|
title: pageTitle,
|
||||||
|
description: truncatedDescription,
|
||||||
|
image: pageImage,
|
||||||
|
canonical,
|
||||||
|
siteName,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if (app) {
|
||||||
|
const pageImage =
|
||||||
|
getSeoImage("ogImage") + constructAppImage({ ...app, description: truncatedDescription });
|
||||||
seoObject = buildSeoMeta({
|
seoObject = buildSeoMeta({
|
||||||
title: pageTitle,
|
title: pageTitle,
|
||||||
description: truncatedDescription,
|
description: truncatedDescription,
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import Badge from "@calcom/ui/Badge";
|
import Badge from "@calcom/ui/Badge";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
/** @deprecated Use `packages/features/ee/teams/components/DisableTeamImpersonation.tsx` */
|
/** @deprecated Use `packages/features/ee/teams/components/DisableTeamImpersonation.tsx` */
|
||||||
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
|
const DisableTeamImpersonation = ({ teamId, memberId }: { teamId: number; memberId: number }) => {
|
||||||
|
|||||||
@@ -6,7 +6,6 @@ import { useState } from "react";
|
|||||||
import TeamAvailabilityModal from "@calcom/features/ee/teams/components/TeamAvailabilityModal";
|
import TeamAvailabilityModal from "@calcom/features/ee/teams/components/TeamAvailabilityModal";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
||||||
@@ -19,6 +18,7 @@ import Dropdown, {
|
|||||||
} from "@calcom/ui/Dropdown";
|
} from "@calcom/ui/Dropdown";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { Tooltip } from "@calcom/ui/Tooltip";
|
import { Tooltip } from "@calcom/ui/Tooltip";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
|
import useCurrentUserId from "@lib/hooks/useCurrentUserId";
|
||||||
|
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ export default function TeamListItem(props: Props) {
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild className="radix-state-open:rounded-r-md">
|
||||||
<Button type="button" color="secondary" size="icon" StartIcon={Icon.FiMoreHorizontal} />
|
<Button type="button" color="secondary" size="icon" StartIcon={Icon.FiMoreHorizontal} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent hidden={hideDropdown}>
|
<DropdownMenuContent hidden={hideDropdown}>
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
import React, { useRef, useState } from "react";
|
import React, { useRef, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { objectKeys } from "@calcom/lib/objectKeys";
|
import { objectKeys } from "@calcom/lib/objectKeys";
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -9,6 +8,7 @@ import { Alert } from "@calcom/ui/Alert";
|
|||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import { TextField } from "@calcom/ui/form/fields";
|
import { TextField } from "@calcom/ui/form/fields";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import ImageUploader from "@components/ImageUploader";
|
import ImageUploader from "@components/ImageUploader";
|
||||||
import SettingInputContainer from "@components/ui/SettingInputContainer";
|
import SettingInputContainer from "@components/ui/SettingInputContainer";
|
||||||
|
|||||||
@@ -3,12 +3,12 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
import { TeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@calcom/ui/ConfirmationDialogContent";
|
||||||
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,5 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import Button from "@calcom/ui/Button";
|
import Button from "@calcom/ui/Button";
|
||||||
@@ -12,6 +11,7 @@ import {
|
|||||||
DialogFooter,
|
DialogFooter,
|
||||||
DialogHeader,
|
DialogHeader,
|
||||||
} from "@calcom/ui/Dialog";
|
} from "@calcom/ui/Dialog";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { useLocale } from "@lib/hooks/useLocale";
|
import { useLocale } from "@lib/hooks/useLocale";
|
||||||
|
|
||||||
|
|||||||
@@ -4,16 +4,19 @@ interface ILinkTextProps {
|
|||||||
href: string;
|
href: string;
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
classNameChildren?: string;
|
classNameChildren?: string;
|
||||||
|
target?: string;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
* This component had to be made in order to make i18n work with next/link
|
* This component had to be made in order to make i18n work with next/link
|
||||||
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
* @see https://github.com/i18next/react-i18next/issues/1090#issuecomment-615426145
|
||||||
**/
|
**/
|
||||||
export const LinkText = (props: ILinkTextProps) => {
|
export const LinkText = (props: ILinkTextProps) => {
|
||||||
const { href, children, classNameChildren, ...moreProps } = props;
|
const { target, href, children, classNameChildren, ...moreProps } = props;
|
||||||
return (
|
return (
|
||||||
<Link href={href || ""} {...moreProps}>
|
<Link href={href || ""} {...moreProps}>
|
||||||
<a className={classNameChildren}>{children}</a>
|
<a target={target || ""} className={classNameChildren}>
|
||||||
|
{children}
|
||||||
|
</a>
|
||||||
</Link>
|
</Link>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { FormValues } from "pages/event-types/[type]";
|
import { FormValues } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
|
import { SingleValueProps, OptionProps, components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
@@ -7,6 +8,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
|
import { Badge } from "@calcom/ui/v2";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
import Select from "@calcom/ui/v2/core/form/select";
|
import Select from "@calcom/ui/v2/core/form/select";
|
||||||
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
import { SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||||
@@ -16,6 +18,35 @@ import { SelectSkeletonLoader } from "@components/v2/availability/SkeletonLoader
|
|||||||
type AvailabilityOption = {
|
type AvailabilityOption = {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
|
isDefault: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault } = props.data;
|
||||||
|
return (
|
||||||
|
<components.Option {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
Default
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.Option>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
|
const { label, isDefault } = props.data;
|
||||||
|
return (
|
||||||
|
<components.SingleValue {...props}>
|
||||||
|
<span>{label}</span>
|
||||||
|
{isDefault && (
|
||||||
|
<Badge variant="blue" className="ml-2">
|
||||||
|
Default
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</components.SingleValue>
|
||||||
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailabilitySelect = ({
|
const AvailabilitySelect = ({
|
||||||
@@ -38,6 +69,7 @@ const AvailabilitySelect = ({
|
|||||||
const options = schedules.map((schedule) => ({
|
const options = schedules.map((schedule) => ({
|
||||||
value: schedule.id,
|
value: schedule.id,
|
||||||
label: schedule.name,
|
label: schedule.name,
|
||||||
|
isDefault: schedule.isDefault,
|
||||||
}));
|
}));
|
||||||
|
|
||||||
const value = options.find((option) =>
|
const value = options.find((option) =>
|
||||||
@@ -53,6 +85,8 @@ const AvailabilitySelect = ({
|
|||||||
onChange={props.onChange}
|
onChange={props.onChange}
|
||||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
||||||
value={value}
|
value={value}
|
||||||
|
components={{ Option, SingleValue }}
|
||||||
|
isMulti={false}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -95,45 +129,50 @@ export const AvailabilityTab = () => {
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="space-y-4 rounded border p-8 py-6 pt-2">
|
<div className="space-y-4 rounded border p-4 py-6 pt-2 md:p-8">
|
||||||
<ol className="table border-collapse text-sm">
|
<ol className="table border-collapse text-sm">
|
||||||
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
{weekdayNames(i18n.language, 1, "long").map((day, index) => {
|
||||||
const isAvailable = !!filterDays(index).length;
|
const isAvailable = !!filterDays(index).length;
|
||||||
return (
|
return (
|
||||||
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
<li key={day} className="my-6 flex border-transparent last:mb-2">
|
||||||
<span className={classNames("w-32 font-medium", !isAvailable && "text-gray-500 opacity-50")}>
|
<span
|
||||||
|
className={classNames(
|
||||||
|
"w-20 font-medium sm:w-32",
|
||||||
|
!isAvailable && "text-gray-500 opacity-50"
|
||||||
|
)}>
|
||||||
{day}
|
{day}
|
||||||
</span>
|
</span>
|
||||||
{isLoading ? (
|
{isLoading ? (
|
||||||
<SkeletonText className="block h-5 w-60" />
|
<SkeletonText className="block h-5 w-60" />
|
||||||
) : isAvailable ? (
|
) : isAvailable ? (
|
||||||
<div className="space-y-3">
|
<div className="space-y-3 text-right">
|
||||||
{filterDays(index).map((dayRange, i) => (
|
{filterDays(index).map((dayRange, i) => (
|
||||||
<div key={i} className="flex items-center leading-4">
|
<div key={i} className="flex items-center leading-4">
|
||||||
<span className="w-28">{format(dayRange.startTime)}</span>
|
<span className="w-16 sm:w-28 sm:text-left">{format(dayRange.startTime)}</span>
|
||||||
<span className="">-</span>
|
<span className="ml-4">-</span>
|
||||||
<div className="ml-6">{format(dayRange.endTime)}</div>
|
<div className="ml-6">{format(dayRange.endTime)}</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<span className="text-gray-500 opacity-50 ">{t("unavailable")}</span>
|
<span className="ml-6 text-gray-500 opacity-50 sm:ml-0">{t("unavailable")}</span>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
</ol>
|
</ol>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="flex justify-between">
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
<span className="flex items-center text-sm text-gray-600">
|
<span className="flex items-center justify-center text-sm text-gray-600 sm:justify-start">
|
||||||
<Icon.FiGlobe className="mr-2" />
|
<Icon.FiGlobe className="mr-2" />
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
</span>
|
</span>
|
||||||
<Button
|
<Button
|
||||||
href={`/availability/${scheduleId}`}
|
href={`/availability/${schedule?.schedule.id}`}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
EndIcon={Icon.FiExternalLink}
|
EndIcon={Icon.FiExternalLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
|
className="justify-center border sm:border-0"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
{t("edit_availability")}
|
{t("edit_availability")}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import { ErrorMessage } from "@hookform/error-message";
|
import { ErrorMessage } from "@hookform/error-message";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { Controller, useForm, useWatch } from "react-hook-form";
|
import { Controller, useForm, useWatch } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -24,6 +25,7 @@ import { Select } from "@calcom/ui/v2/";
|
|||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
|
import { LinkText } from "@components/ui/LinkText";
|
||||||
import CheckboxField from "@components/ui/form/CheckboxField";
|
import CheckboxField from "@components/ui/form/CheckboxField";
|
||||||
|
|
||||||
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
type BookingItem = inferQueryOutput<"viewer.bookings">["bookings"][number];
|
||||||
@@ -308,6 +310,17 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
{selectedLocation && LocationOptions}
|
{selectedLocation && LocationOptions}
|
||||||
|
<div className="mt-5">
|
||||||
|
<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
|
||||||
|
<LinkText href="/apps/categories/video" classNameChildren="text-blue-400" target="_blank">
|
||||||
|
App Store
|
||||||
|
</LinkText>
|
||||||
|
.
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
<div className="mt-4 flex justify-end space-x-2">
|
<div className="mt-4 flex justify-end space-x-2">
|
||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -1,12 +1,13 @@
|
|||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInput } from "@prisma/client/";
|
import { EventTypeCustomInput } from "@prisma/client/";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import short from "short-uuid";
|
import short from "short-uuid";
|
||||||
import { v5 as uuidv5 } from "uuid";
|
import { v5 as uuidv5 } from "uuid";
|
||||||
|
|
||||||
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -14,16 +15,17 @@ import { Icon } from "@calcom/ui";
|
|||||||
import {
|
import {
|
||||||
Button,
|
Button,
|
||||||
CustomInputItem,
|
CustomInputItem,
|
||||||
DestinationCalendarSelector,
|
|
||||||
Dialog,
|
Dialog,
|
||||||
DialogContent,
|
DialogContent,
|
||||||
Label,
|
Label,
|
||||||
|
SettingsToggle,
|
||||||
showToast,
|
showToast,
|
||||||
Skeleton,
|
Skeleton,
|
||||||
Switch,
|
Switch,
|
||||||
TextField,
|
TextField,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@calcom/ui/v2";
|
} from "@calcom/ui/v2";
|
||||||
|
import CheckboxField from "@calcom/ui/v2/core/form/Checkbox";
|
||||||
|
|
||||||
import CustomInputTypeForm from "@components/v2/eventtype/CustomInputTypeForm";
|
import CustomInputTypeForm from "@components/v2/eventtype/CustomInputTypeForm";
|
||||||
|
|
||||||
@@ -42,7 +44,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
const [seatsInputVisible, setSeatsInputVisible] = useState(!!eventType.seatsPerTimeSlot);
|
|
||||||
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
const [customInputs, setCustomInputs] = useState<EventTypeCustomInput[]>(
|
||||||
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
eventType.customInputs.sort((a, b) => a.id - b.id) || []
|
||||||
);
|
);
|
||||||
@@ -50,13 +51,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
const [selectedCustomInputModalOpen, setSelectedCustomInputModalOpen] = useState(false);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
|
|
||||||
const animationRef = useRef(null);
|
|
||||||
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.getValues("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
animationRef.current && autoAnimate(animationRef.current);
|
|
||||||
}, [animationRef]);
|
|
||||||
|
|
||||||
const removeCustom = (index: number) => {
|
const removeCustom = (index: number) => {
|
||||||
formMethods.getValues("customInputs").splice(index, 1);
|
formMethods.getValues("customInputs").splice(index, 1);
|
||||||
customInputs.splice(index, 1);
|
customInputs.splice(index, 1);
|
||||||
@@ -122,80 +118,61 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<div className="">
|
<div className="">
|
||||||
<div className="flex space-x-3 ">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("additional_inputs")}
|
||||||
checked={customInputs.length > 0}
|
description={t("additional_input_description")}
|
||||||
fitToHeight={true}
|
checked={customInputs.length > 0}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
if (e && customInputs.length === 0) {
|
if (e && customInputs.length === 0) {
|
||||||
// Push a placeholders
|
// Push a placeholders
|
||||||
setSelectedCustomInput(undefined);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
} else if (!e) {
|
|
||||||
setCustomInputs([]);
|
|
||||||
formMethods.setValue("customInputs", []);
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("additional_inputs")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("additional_input_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ul className="my-4" ref={animationRef}>
|
|
||||||
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
|
||||||
<CustomInputItem
|
|
||||||
key={idx}
|
|
||||||
question={customInput.label}
|
|
||||||
type={customInput.type}
|
|
||||||
required={customInput.required}
|
|
||||||
editOnClick={() => {
|
|
||||||
setSelectedCustomInput(customInput);
|
|
||||||
setSelectedCustomInputModalOpen(true);
|
|
||||||
}}
|
|
||||||
deleteOnClick={() => removeCustom(idx)}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
{customInputs.length > 0 && (
|
|
||||||
<Button
|
|
||||||
StartIcon={Icon.FiPlus}
|
|
||||||
color="minimal"
|
|
||||||
type="button"
|
|
||||||
onClick={() => {
|
|
||||||
setSelectedCustomInput(undefined);
|
setSelectedCustomInput(undefined);
|
||||||
setSelectedCustomInputModalOpen(true);
|
setSelectedCustomInputModalOpen(true);
|
||||||
}}>
|
} else if (!e) {
|
||||||
Add an input
|
setCustomInputs([]);
|
||||||
</Button>
|
formMethods.setValue("customInputs", []);
|
||||||
)}
|
}
|
||||||
|
}}>
|
||||||
|
<ul className="my-4">
|
||||||
|
{customInputs.map((customInput: EventTypeCustomInput, idx: number) => (
|
||||||
|
<CustomInputItem
|
||||||
|
key={idx}
|
||||||
|
question={customInput.label}
|
||||||
|
type={customInput.type}
|
||||||
|
required={customInput.required}
|
||||||
|
editOnClick={() => {
|
||||||
|
setSelectedCustomInput(customInput);
|
||||||
|
setSelectedCustomInputModalOpen(true);
|
||||||
|
}}
|
||||||
|
deleteOnClick={() => removeCustom(idx)}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
{customInputs.length > 0 && (
|
||||||
|
<Button
|
||||||
|
StartIcon={Icon.FiPlus}
|
||||||
|
color="minimal"
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
setSelectedCustomInput(undefined);
|
||||||
|
setSelectedCustomInputModalOpen(true);
|
||||||
|
}}>
|
||||||
|
Add an input
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</SettingsToggle>
|
||||||
</div>
|
</div>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
name="requiresConfirmation"
|
name="requiresConfirmation"
|
||||||
defaultValue={eventType.requiresConfirmation}
|
defaultValue={eventType.requiresConfirmation}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex space-x-3">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("requires_confirmation")}
|
||||||
name="requireConfirmation"
|
description={t("requires_confirmation_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
disabled={seatsEnabled}
|
disabled={seatsEnabled}
|
||||||
fitToHeight={true}
|
/>
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("requires_confirmation")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("requires_confirmation_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -204,23 +181,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={eventType.disableGuests}
|
defaultValue={eventType.disableGuests}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex space-x-3 ">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("disable_guests")}
|
||||||
name="disableGuests"
|
description={t("disable_guests_description")}
|
||||||
fitToHeight={true}
|
checked={value}
|
||||||
checked={value}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
disabled={seatsEnabled}
|
||||||
disabled={seatsEnabled}
|
/>
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("disable_guests")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("disable_guests_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
@@ -230,22 +197,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={eventType.hideCalendarNotes}
|
defaultValue={eventType.hideCalendarNotes}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex space-x-3 ">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("disable_notes")}
|
||||||
name="hideCalendarNotes"
|
description={t("disable_notes_description")}
|
||||||
fitToHeight={true}
|
checked={value}
|
||||||
checked={value}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
/>
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("disable_notes")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("disable_notes_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
@@ -255,20 +212,12 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
defaultValue={!!eventType.metadata.additionalNotesRequired}
|
defaultValue={!!eventType.metadata.additionalNotesRequired}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex space-x-3 ">
|
<div className="flex space-x-3 ">
|
||||||
<Switch
|
<SettingsToggle
|
||||||
name="additionalNotesRequired"
|
title={t("require_additional_notes")}
|
||||||
fitToHeight={true}
|
description={t("require_additional_notes_description")}
|
||||||
checked={value}
|
checked={!!value}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("require_additional_notes")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("require_additional_notes_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -276,32 +225,21 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={hashedUrl}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<>
|
<>
|
||||||
<div className="flex space-x-3 ">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("redirect_success_booking")}
|
||||||
name="successRedirectUrlCheck"
|
description={t("redirect_url_description")}
|
||||||
fitToHeight={true}
|
checked={redirectUrlVisible}
|
||||||
defaultChecked={redirectUrlVisible}
|
onCheckedChange={(e) => {
|
||||||
onCheckedChange={(e) => {
|
setRedirectUrlVisible(e);
|
||||||
setRedirectUrlVisible(e);
|
onChange(e ? value : "");
|
||||||
onChange(e ? value : "");
|
}}>
|
||||||
}}
|
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||||
/>
|
<div className="lg:-ml-2">
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("redirect_success_booking")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("redirect_url_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
{redirectUrlVisible && (
|
|
||||||
<div className="">
|
|
||||||
<TextField
|
<TextField
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
|
labelSrOnly
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
required={redirectUrlVisible}
|
required={redirectUrlVisible}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -309,130 +247,107 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupInfered
|
|||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
</SettingsToggle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
|
||||||
name="hashedLink"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={hashedUrl}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<>
|
|
||||||
<div className="flex space-x-3 ">
|
|
||||||
<Switch
|
|
||||||
data-testid="hashedLinkCheck"
|
|
||||||
name="hashedLinkCheck"
|
|
||||||
fitToHeight={true}
|
|
||||||
defaultChecked={!!value}
|
|
||||||
onCheckedChange={(e) => {
|
|
||||||
setHashedLinkVisible(e);
|
|
||||||
onChange(e ? hashedUrl : undefined);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<div className="flex flex-col">
|
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("private_link")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("private_link_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
{hashedLinkVisible && (
|
<SettingsToggle
|
||||||
<div className="">
|
data-testid="hashedLinkCheck"
|
||||||
<TextField
|
title={t("private_link")}
|
||||||
disabled
|
description={t("private_link_description")}
|
||||||
name="hashedLink"
|
checked={hashedLinkVisible}
|
||||||
label={t("private_link_label")}
|
onCheckedChange={(e) => {
|
||||||
data-testid="generated-hash-url"
|
formMethods.setValue("hashedLink", e ? hashedUrl : undefined);
|
||||||
type="text"
|
setHashedLinkVisible(e);
|
||||||
hint={t("private_link_hint")}
|
}}>
|
||||||
defaultValue={placeholderHashedLink}
|
{/* Textfield has some margin by default we remove that so we can keep consitant aligment */}
|
||||||
addOnSuffix={
|
<div className="lg:-ml-2">
|
||||||
<Tooltip
|
<TextField
|
||||||
content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
disabled
|
||||||
<Button
|
name="hashedLink"
|
||||||
color="minimal"
|
label={t("private_link_label")}
|
||||||
onClick={() => {
|
data-testid="generated-hash-url"
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
labelSrOnly
|
||||||
if (eventType.hashedLink) {
|
type="text"
|
||||||
showToast(t("private_link_copied"), "success");
|
hint={t("private_link_hint")}
|
||||||
} else {
|
defaultValue={placeholderHashedLink}
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
addOnSuffix={
|
||||||
}
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
}}
|
<Button
|
||||||
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
color="minimal"
|
||||||
type="button">
|
onClick={() => {
|
||||||
<Icon.FiCopy />
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
</Button>
|
if (eventType.hashedLink) {
|
||||||
</Tooltip>
|
showToast(t("private_link_copied"), "success");
|
||||||
}
|
} else {
|
||||||
/>
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
</div>
|
}
|
||||||
)}
|
}}
|
||||||
</>
|
className="hover:stroke-3 hover:bg-transparent hover:text-black"
|
||||||
)}
|
type="button">
|
||||||
/>
|
<Icon.FiCopy />
|
||||||
|
</Button>
|
||||||
|
</Tooltip>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</SettingsToggle>
|
||||||
<hr />
|
<hr />
|
||||||
<Controller
|
<Controller
|
||||||
name="seatsPerTimeSlotEnabled"
|
name="seatsPerTimeSlotEnabled"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div className="flex space-x-3">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("offer_seats")}
|
||||||
name="seatsPerTimeSlotEnabled"
|
description={t("offer_seats_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
setSeatsInputVisible(e);
|
// Enabling seats will disable guests and requiring confimation until fully supported
|
||||||
// Enabling seats will disable guests and requiring confimation until fully supported
|
if (e) {
|
||||||
if (e) {
|
formMethods.setValue("disableGuests", true);
|
||||||
formMethods.setValue("disableGuests", true);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
} else {
|
||||||
} else {
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("disableGuests", false);
|
||||||
}
|
}
|
||||||
onChange(e);
|
onChange(e);
|
||||||
}}
|
}}>
|
||||||
fitToHeight={true}
|
<Controller
|
||||||
|
name="seatsPerTimeSlot"
|
||||||
|
control={formMethods.control}
|
||||||
|
defaultValue={eventType.seatsPerTimeSlot}
|
||||||
|
render={({ field: { value, onChange } }) => (
|
||||||
|
<div className="lg:-ml-2">
|
||||||
|
<TextField
|
||||||
|
required
|
||||||
|
name="seatsPerTimeSlot"
|
||||||
|
labelSrOnly
|
||||||
|
label={t("number_of_seats")}
|
||||||
|
type="number"
|
||||||
|
defaultValue={value || 2}
|
||||||
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
|
onChange={(e) => {
|
||||||
|
onChange(Number(e.target.value));
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<CheckboxField
|
||||||
|
description={t("show_attendees")}
|
||||||
|
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||||
|
defaultChecked={!!eventType.seatsShowAttendees}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
<div className="flex flex-col">
|
</SettingsToggle>
|
||||||
<Skeleton as={Label} className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("offer_seats")}
|
|
||||||
</Skeleton>
|
|
||||||
<Skeleton as="p" className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("offer_seats_description")}
|
|
||||||
</Skeleton>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{seatsInputVisible && (
|
|
||||||
<Controller
|
|
||||||
name="seatsPerTimeSlot"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.seatsPerTimeSlot}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
|
||||||
<div className="">
|
|
||||||
<TextField
|
|
||||||
required
|
|
||||||
name="seatsPerTimeSlot"
|
|
||||||
label={t("number_of_seats")}
|
|
||||||
type="number"
|
|
||||||
defaultValue={value || 2}
|
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
|
||||||
onChange={(e) => {
|
|
||||||
onChange(Number(e.target.value));
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
<Dialog open={showEventNameTip} onOpenChange={setShowEventNameTip}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFormContext, Controller, useWatch } from "react-hook-form";
|
import { useFormContext, Controller, useWatch } from "react-hook-form";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
@@ -9,7 +9,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { PeriodType } from "@calcom/prisma/client";
|
import { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { BookingLimit } from "@calcom/types/Calendar";
|
import type { BookingLimit } from "@calcom/types/Calendar";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import { Select, Switch, Label, Input, MinutesField, Button } from "@calcom/ui/v2";
|
import { Select, Switch, Label, Input, MinutesField, Button, SettingsToggle } from "@calcom/ui/v2";
|
||||||
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
import DateRangePicker from "@calcom/ui/v2/core/form/date-range-picker/DateRangePicker";
|
||||||
|
|
||||||
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
|
export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">) => {
|
||||||
@@ -159,149 +159,106 @@ export const EventLimitsTab = (props: Pick<EventTypeSetupInfered, "eventType">)
|
|||||||
|
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
|
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<Controller
|
||||||
<fieldset className="block flex-col sm:flex">
|
name="bookingLimits"
|
||||||
<div className="flex space-x-3">
|
control={formMethods.control}
|
||||||
<Controller
|
render={({ field: { value } }) => (
|
||||||
name="bookingLimits"
|
<SettingsToggle
|
||||||
control={formMethods.control}
|
title={t("limit_booking_frequency")}
|
||||||
render={({ field: { value } }) => (
|
description={t("limit_booking_frequency_description")}
|
||||||
<Switch
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
fitToHeight={true}
|
onCheckedChange={(active) => {
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
if (active) {
|
||||||
onCheckedChange={(active) => {
|
formMethods.setValue("bookingLimits", {
|
||||||
if (active) {
|
PER_DAY: 1,
|
||||||
formMethods.setValue("bookingLimits", {
|
});
|
||||||
PER_DAY: 1,
|
} else {
|
||||||
});
|
formMethods.setValue("bookingLimits", {});
|
||||||
} else {
|
}
|
||||||
formMethods.setValue("bookingLimits", undefined);
|
}}>
|
||||||
}
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<div className="">
|
|
||||||
<Label className="text-sm font-semibold leading-none text-black">
|
|
||||||
{t("limit_booking_frequency")}
|
|
||||||
</Label>
|
|
||||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
|
||||||
{t("limit_booking_frequency_description")}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 lg:ml-14">
|
|
||||||
<BookingLimits />
|
<BookingLimits />
|
||||||
</div>
|
</SettingsToggle>
|
||||||
</fieldset>
|
)}
|
||||||
</div>
|
/>
|
||||||
|
|
||||||
<hr className="my-8" />
|
<hr className="my-8" />
|
||||||
|
<Controller
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
name="periodType"
|
||||||
<fieldset className="block flex-col sm:flex">
|
control={formMethods.control}
|
||||||
<div className="flex space-x-3">
|
render={({ field: { value } }) => (
|
||||||
<Controller
|
<SettingsToggle
|
||||||
name="periodType"
|
title={t("limit_future_bookings")}
|
||||||
control={formMethods.control}
|
description={t("limit_future_bookings_description")}
|
||||||
defaultValue={periodType?.type !== "UNLIMITED" ? "ROLLING" : "UNLIMITED"}
|
checked={value !== "UNLIMITED"}
|
||||||
render={({ field: { value } }) => (
|
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||||
<Switch
|
<RadioGroup.Root
|
||||||
fitToHeight={true}
|
defaultValue={watchPeriodType}
|
||||||
checked={value !== "UNLIMITED"}
|
value={watchPeriodType}
|
||||||
onCheckedChange={(bool) =>
|
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||||
formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")
|
{PERIOD_TYPES.map((period) => {
|
||||||
}
|
if (period.type === "UNLIMITED") return null;
|
||||||
/>
|
return (
|
||||||
)}
|
<div
|
||||||
/>
|
className={classNames(
|
||||||
|
"mb-2 flex flex-wrap items-center text-sm",
|
||||||
<div className="">
|
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||||
<Label className="text-sm font-semibold leading-none text-black">Limit Future Bookings</Label>
|
)}
|
||||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
key={period.type}>
|
||||||
Limit how far in the future people can book a time
|
<RadioGroup.Item
|
||||||
</p>
|
id={period.type}
|
||||||
</div>
|
value={period.type}
|
||||||
</div>
|
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
<div className="mt-4 lg:ml-14">
|
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
||||||
<Controller
|
</RadioGroup.Item>
|
||||||
name="periodType"
|
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||||
control={formMethods.control}
|
{period.type === "ROLLING" && (
|
||||||
defaultValue={periodType?.type}
|
<div className="flex h-9">
|
||||||
render={() => (
|
<Input
|
||||||
<RadioGroup.Root
|
type="number"
|
||||||
defaultValue={watchPeriodType}
|
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
value={watchPeriodType}
|
placeholder="30"
|
||||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
{PERIOD_TYPES.map((period) => {
|
defaultValue={eventType.periodDays || 30}
|
||||||
if (period.type === "UNLIMITED") return null;
|
/>
|
||||||
return (
|
<select
|
||||||
<div
|
id=""
|
||||||
className={classNames(
|
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
||||||
"mb-2 flex flex-wrap items-center text-sm",
|
{...formMethods.register("periodCountCalendarDays")}
|
||||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||||
)}
|
<option value="1">{t("calendar_days")}</option>
|
||||||
key={period.type}>
|
<option value="0">{t("business_days")}</option>
|
||||||
<RadioGroup.Item
|
</select>
|
||||||
id={period.type}
|
|
||||||
value={period.type}
|
|
||||||
className="min-w-4 flex h-4 w-4 cursor-pointer items-center rounded-full border border-black bg-white focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
|
||||||
<RadioGroup.Indicator className="relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full after:bg-black" />
|
|
||||||
</RadioGroup.Item>
|
|
||||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
|
||||||
{period.type === "ROLLING" && (
|
|
||||||
<div className="flex h-9">
|
|
||||||
<Input
|
|
||||||
type="number"
|
|
||||||
className="block w-16 rounded-md border-gray-300 py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
|
||||||
placeholder="30"
|
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
|
||||||
defaultValue={eventType.periodDays || 30}
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
id=""
|
|
||||||
className="block h-9 w-full rounded-md border-gray-300 py-2 pl-3 pr-10 text-sm focus:outline-none"
|
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
|
||||||
<option value="1">{t("calendar_days")}</option>
|
|
||||||
<option value="0">{t("business_days")}</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{period.type === "RANGE" && (
|
|
||||||
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
|
||||||
<Controller
|
|
||||||
name="periodDates"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={periodDates}
|
|
||||||
render={() => (
|
|
||||||
<DateRangePicker
|
|
||||||
startDate={formMethods.getValues("periodDates").startDate}
|
|
||||||
endDate={formMethods.getValues("periodDates").endDate}
|
|
||||||
onDatesChange={({ startDate, endDate }) => {
|
|
||||||
formMethods.setValue("periodDates", {
|
|
||||||
startDate,
|
|
||||||
endDate,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{period.suffix ? (
|
|
||||||
<span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span>
|
|
||||||
) : null}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
)}
|
||||||
})}
|
{period.type === "RANGE" && (
|
||||||
</RadioGroup.Root>
|
<div className="inline-flex space-x-2 ltr:ml-2 rtl:mr-2 rtl:space-x-reverse">
|
||||||
)}
|
<Controller
|
||||||
/>
|
name="periodDates"
|
||||||
</div>
|
control={formMethods.control}
|
||||||
</fieldset>
|
defaultValue={periodDates}
|
||||||
</div>
|
render={() => (
|
||||||
|
<DateRangePicker
|
||||||
|
startDate={formMethods.getValues("periodDates").startDate}
|
||||||
|
endDate={formMethods.getValues("periodDates").endDate}
|
||||||
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
|
formMethods.setValue("periodDates", {
|
||||||
|
startDate,
|
||||||
|
endDate,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{period.suffix ? <span className="ltr:ml-2 rtl:mr-2"> {period.suffix}</span> : null}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</RadioGroup.Root>
|
||||||
|
</SettingsToggle>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -311,12 +268,9 @@ type BookingLimitsKey = keyof BookingLimit;
|
|||||||
const BookingLimits = () => {
|
const BookingLimits = () => {
|
||||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||||
const watchBookingLimits = watch("bookingLimits");
|
const watchBookingLimits = watch("bookingLimits");
|
||||||
const animateRef = useRef(null);
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
useEffect(() => {
|
const [animateRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
animateRef.current && autoAnimate(animateRef.current);
|
|
||||||
}, [animateRef]);
|
|
||||||
|
|
||||||
const BOOKING_LIMIT_OPTIONS: {
|
const BOOKING_LIMIT_OPTIONS: {
|
||||||
value: keyof BookingLimit;
|
value: keyof BookingLimit;
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { isValidPhoneNumber } from "libphonenumber-js";
|
import { isValidPhoneNumber } from "libphonenumber-js";
|
||||||
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
import { EventTypeSetupInfered, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { Controller, useForm, useFormContext } from "react-hook-form";
|
import { Controller, useForm, useFormContext } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -85,11 +85,8 @@ export const EventSetupTab = (
|
|||||||
|
|
||||||
const Locations = () => {
|
const Locations = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const animationRef = useRef(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
const [animationRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
animationRef.current && autoAnimate(animationRef.current);
|
|
||||||
}, [animationRef]);
|
|
||||||
|
|
||||||
const validLocations = formMethods.getValues("locations").filter((location) => {
|
const validLocations = formMethods.getValues("locations").filter((location) => {
|
||||||
const eventLocation = getEventLocationType(location.type);
|
const eventLocation = getEventLocationType(location.type);
|
||||||
@@ -135,14 +132,14 @@ export const EventSetupTab = (
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
|
<li key={location.type} className="mb-2 rounded-md border border-neutral-300 py-1.5 px-2">
|
||||||
<div className="flex justify-between">
|
<div className="flex max-w-full justify-between">
|
||||||
<div key={index} className="flex flex-grow items-center">
|
<div key={index} className="flex flex-grow items-center">
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
className="h-6 w-6"
|
className="h-6 w-6"
|
||||||
alt={`${eventLocationType.label} logo`}
|
alt={`${eventLocationType.label} logo`}
|
||||||
/>
|
/>
|
||||||
<span className="text-sm ltr:ml-2 rtl:mr-2">
|
<span className="truncate text-sm ltr:ml-2 rtl:mr-2">
|
||||||
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
{location[eventLocationType.defaultValueVariable] || eventLocationType.label}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
} from "@calcom/ui/v2";
|
} from "@calcom/ui/v2";
|
||||||
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
|
import ConfirmationDialogContent from "@calcom/ui/v2/core/ConfirmationDialogContent";
|
||||||
import { Dialog } from "@calcom/ui/v2/core/Dialog";
|
import { Dialog } from "@calcom/ui/v2/core/Dialog";
|
||||||
|
import Divider from "@calcom/ui/v2/core/Divider";
|
||||||
import Dropdown, {
|
import Dropdown, {
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
@@ -172,11 +173,11 @@ function EventTypeSingleLayout({
|
|||||||
subtitle={eventType.description || ""}
|
subtitle={eventType.description || ""}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="flex items-center rounded-md px-2 sm:hover:bg-gray-100">
|
<div className="hidden items-center rounded-md px-2 sm:flex sm:hover:bg-gray-100">
|
||||||
<Skeleton
|
<Skeleton
|
||||||
as={Label}
|
as={Label}
|
||||||
htmlFor="hiddenSwitch"
|
htmlFor="hiddenSwitch"
|
||||||
className="mt-2 hidden cursor-pointer self-center pr-2 sm:inline">
|
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||||
{t("hide_from_profile")}
|
{t("hide_from_profile")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -231,10 +232,10 @@ function EventTypeSingleLayout({
|
|||||||
/>
|
/>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<VerticalDivider />
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger className="focus:ring-brand-900 block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 focus:bg-gray-100 focus:outline-none focus:ring-2 focus:ring-offset-1 lg:hidden">
|
<DropdownMenuTrigger className="block h-9 w-9 justify-center rounded-md border border-gray-200 bg-transparent text-gray-700 lg:hidden">
|
||||||
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" />
|
<Icon.FiMoreHorizontal className="group-hover:text-gray-800" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
@@ -264,6 +265,22 @@ function EventTypeSingleLayout({
|
|||||||
{t("delete")}
|
{t("delete")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
<Divider />
|
||||||
|
<div className="flex items-center rounded-md py-1.5 px-4 sm:hidden sm:hover:bg-gray-100">
|
||||||
|
<Skeleton
|
||||||
|
as={Label}
|
||||||
|
htmlFor="hiddenSwitch"
|
||||||
|
className="mt-2 inline cursor-pointer self-center pr-2 sm:hidden">
|
||||||
|
{t("hide_from_profile")}
|
||||||
|
</Skeleton>
|
||||||
|
<Switch
|
||||||
|
id="hiddenSwitch"
|
||||||
|
defaultChecked={formMethods.getValues("hidden")}
|
||||||
|
onCheckedChange={(e) => {
|
||||||
|
formMethods.setValue("hidden", e);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
<div className="border-l-2 border-gray-300" />
|
<div className="border-l-2 border-gray-300" />
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import { Label, Select, Switch } from "@calcom/ui/v2";
|
import { Label, Select, Switch, SettingsToggle } from "@calcom/ui/v2";
|
||||||
|
|
||||||
type RecurringEventControllerProps = {
|
type RecurringEventControllerProps = {
|
||||||
recurringEvent: RecurringEvent | null;
|
recurringEvent: RecurringEvent | null;
|
||||||
@@ -36,95 +36,85 @@ export default function RecurringEventController({
|
|||||||
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
|
<Alert severity="warning" title={t("warning_payment_recurring_event")} />
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<div className="flex space-x-3 ">
|
<SettingsToggle
|
||||||
<Switch
|
title={t("recurring_event")}
|
||||||
name="requireConfirmation"
|
description={t("recurring_event_description")}
|
||||||
data-testid="recurring-event-check"
|
checked={recurringEventState !== null}
|
||||||
fitToHeight={true}
|
data-testid="recurring-event-check"
|
||||||
checked={recurringEventState !== null}
|
onCheckedChange={(e) => {
|
||||||
onCheckedChange={(e) => {
|
if (!e) {
|
||||||
if (!e) {
|
formMethods.setValue("recurringEvent", null);
|
||||||
formMethods.setValue("recurringEvent", null);
|
setRecurringEventState(null);
|
||||||
setRecurringEventState(null);
|
} else {
|
||||||
} else {
|
const newVal = recurringEvent || {
|
||||||
const newVal = recurringEvent || {
|
interval: 1,
|
||||||
interval: 1,
|
count: 12,
|
||||||
count: 12,
|
freq: Frequency.WEEKLY,
|
||||||
freq: Frequency.WEEKLY,
|
};
|
||||||
};
|
formMethods.setValue("recurringEvent", newVal);
|
||||||
formMethods.setValue("recurringEvent", newVal);
|
setRecurringEventState(newVal);
|
||||||
setRecurringEventState(newVal);
|
}
|
||||||
}
|
}}>
|
||||||
}}
|
{recurringEventState && (
|
||||||
/>
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex flex-col">
|
<div className="flex items-center">
|
||||||
<Label className="text-sm font-semibold leading-none text-black">
|
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
||||||
{t("recurring_event")}
|
<input
|
||||||
</Label>
|
type="number"
|
||||||
<p className="-mt-2 text-sm leading-normal text-gray-600">
|
min="1"
|
||||||
{t("recurring_event_description")}
|
max="20"
|
||||||
</p>
|
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
{recurringEventState && (
|
defaultValue={recurringEventState.interval}
|
||||||
<div data-testid="recurring-event-collapsible" className="mt-4 text-sm">
|
onChange={(event) => {
|
||||||
<div className="flex items-center">
|
const newVal = {
|
||||||
<p className="mr-2 text-neutral-900">{t("repeats_every")}</p>
|
...recurringEventState,
|
||||||
<input
|
interval: parseInt(event?.target.value),
|
||||||
type="number"
|
};
|
||||||
min="1"
|
formMethods.setValue("recurringEvent", newVal);
|
||||||
max="20"
|
setRecurringEventState(newVal);
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
}}
|
||||||
defaultValue={recurringEventState.interval}
|
/>
|
||||||
onChange={(event) => {
|
<Select
|
||||||
const newVal = {
|
options={recurringEventFreqOptions}
|
||||||
...recurringEventState,
|
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||||
interval: parseInt(event?.target.value),
|
isSearchable={false}
|
||||||
};
|
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
||||||
formMethods.setValue("recurringEvent", newVal);
|
onChange={(event) => {
|
||||||
setRecurringEventState(newVal);
|
const newVal = {
|
||||||
}}
|
...recurringEventState,
|
||||||
/>
|
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
||||||
<Select
|
};
|
||||||
options={recurringEventFreqOptions}
|
formMethods.setValue("recurringEvent", newVal);
|
||||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
setRecurringEventState(newVal);
|
||||||
isSearchable={false}
|
}}
|
||||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
/>
|
||||||
onChange={(event) => {
|
|
||||||
const newVal = {
|
|
||||||
...recurringEventState,
|
|
||||||
freq: parseInt(event?.value || `${Frequency.WEEKLY}`),
|
|
||||||
};
|
|
||||||
formMethods.setValue("recurringEvent", newVal);
|
|
||||||
setRecurringEventState(newVal);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="mt-4 flex items-center">
|
|
||||||
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
|
||||||
<input
|
|
||||||
type="number"
|
|
||||||
min="1"
|
|
||||||
max="20"
|
|
||||||
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultValue={recurringEventState.count}
|
|
||||||
onChange={(event) => {
|
|
||||||
const newVal = {
|
|
||||||
...recurringEventState,
|
|
||||||
count: parseInt(event?.target.value),
|
|
||||||
};
|
|
||||||
formMethods.setValue("recurringEvent", newVal);
|
|
||||||
setRecurringEventState(newVal);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<p className="mr-2 text-neutral-900">
|
|
||||||
{t("events", {
|
|
||||||
count: recurringEventState.count,
|
|
||||||
})}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
)}
|
<div className="mt-4 flex items-center">
|
||||||
</div>
|
<p className="mr-2 text-neutral-900">{t("for_a_maximum_of")}</p>
|
||||||
</div>
|
<input
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
max="20"
|
||||||
|
className="block h-[36px] w-16 rounded-md border-gray-300 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
|
defaultValue={recurringEventState.count}
|
||||||
|
onChange={(event) => {
|
||||||
|
const newVal = {
|
||||||
|
...recurringEventState,
|
||||||
|
count: parseInt(event?.target.value),
|
||||||
|
};
|
||||||
|
formMethods.setValue("recurringEvent", newVal);
|
||||||
|
setRecurringEventState(newVal);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<p className="mr-2 text-neutral-900">
|
||||||
|
{t("events", {
|
||||||
|
count: recurringEventState.count,
|
||||||
|
})}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</SettingsToggle>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,40 +0,0 @@
|
|||||||
import jackson, { IAPIController, IOAuthController, JacksonOption } from "@boxyhq/saml-jackson";
|
|
||||||
|
|
||||||
import { BASE_URL } from "@lib/config/constants";
|
|
||||||
import { samlDatabaseUrl } from "@lib/saml";
|
|
||||||
|
|
||||||
// Set the required options. Refer to https://github.com/boxyhq/jackson#configuration for the full list
|
|
||||||
const opts: JacksonOption = {
|
|
||||||
externalUrl: BASE_URL,
|
|
||||||
samlPath: "/api/auth/saml/callback",
|
|
||||||
db: {
|
|
||||||
engine: "sql",
|
|
||||||
type: "postgres",
|
|
||||||
url: samlDatabaseUrl,
|
|
||||||
encryptionKey: process.env.CALENDSO_ENCRYPTION_KEY,
|
|
||||||
},
|
|
||||||
samlAudience: "https://saml.cal.com",
|
|
||||||
};
|
|
||||||
|
|
||||||
let apiController: IAPIController;
|
|
||||||
let oauthController: IOAuthController;
|
|
||||||
|
|
||||||
const g = global as any;
|
|
||||||
|
|
||||||
export default async function init() {
|
|
||||||
if (!g.apiController || !g.oauthController) {
|
|
||||||
const ret = await jackson(opts);
|
|
||||||
apiController = ret.apiController;
|
|
||||||
oauthController = ret.oauthController;
|
|
||||||
g.apiController = apiController;
|
|
||||||
g.oauthController = oauthController;
|
|
||||||
} else {
|
|
||||||
apiController = g.apiController;
|
|
||||||
oauthController = g.oauthController;
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
apiController,
|
|
||||||
oauthController,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
@@ -1,22 +1,46 @@
|
|||||||
|
import { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
import * as fetch from "@lib/core/http/fetch-wrapper";
|
import * as fetch from "@lib/core/http/fetch-wrapper";
|
||||||
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
import { BookingCreateBody, BookingResponse } from "@lib/types/booking";
|
||||||
|
|
||||||
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
|
type ExtendedBookingCreateBody = BookingCreateBody & { noEmail?: boolean; recurringCount?: number };
|
||||||
|
|
||||||
const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
|
const createRecurringBooking = async (data: ExtendedBookingCreateBody[]) => {
|
||||||
return Promise.all(
|
const createdBookings: BookingResponse[] = [];
|
||||||
data.map((booking, key) => {
|
// Reversing to accumulate results for noEmail instances first, to then lastly, create the
|
||||||
// We only want to send the first occurrence of the meeting at the moment, not all at once
|
// emailed booking taking into account accumulated results to send app status accurately
|
||||||
if (key === 0) {
|
for (let key = 0; key < data.length; key++) {
|
||||||
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", booking);
|
const booking = data[key];
|
||||||
} else {
|
if (key === data.length - 1) {
|
||||||
return fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
|
const calcAppsStatus: { [key: string]: AppsStatus } = createdBookings
|
||||||
...booking,
|
.flatMap((book) => (book.appsStatus !== undefined ? book.appsStatus : []))
|
||||||
noEmail: true,
|
.reduce((prev, curr) => {
|
||||||
});
|
if (prev[curr.type]) {
|
||||||
}
|
prev[curr.type].failures += curr.failures;
|
||||||
})
|
prev[curr.type].success += curr.success;
|
||||||
);
|
} else {
|
||||||
|
prev[curr.type] = curr;
|
||||||
|
}
|
||||||
|
return prev;
|
||||||
|
}, {} as { [key: string]: AppsStatus });
|
||||||
|
const appsStatus = Object.values(calcAppsStatus);
|
||||||
|
const response = await fetch.post<
|
||||||
|
ExtendedBookingCreateBody & { appsStatus: AppsStatus[] },
|
||||||
|
BookingResponse
|
||||||
|
>("/api/book/event", {
|
||||||
|
...booking,
|
||||||
|
appsStatus,
|
||||||
|
});
|
||||||
|
createdBookings.push(response);
|
||||||
|
} else {
|
||||||
|
const response = await fetch.post<ExtendedBookingCreateBody, BookingResponse>("/api/book/event", {
|
||||||
|
...booking,
|
||||||
|
noEmail: true,
|
||||||
|
});
|
||||||
|
createdBookings.push(response);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return createdBookings;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default createRecurringBooking;
|
export default createRecurringBooking;
|
||||||
+18
-14
@@ -20,6 +20,15 @@ export const parseDate = (date: string | null | Dayjs, i18n: I18n) => {
|
|||||||
return processDate(date, i18n);
|
return processDate(date, i18n);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// tzid is currently broken in rrule library.
|
||||||
|
// @see https://github.com/jakubroztocil/rrule/issues/523
|
||||||
|
const dateWithZone = (d: Date, timeZone?: string) => {
|
||||||
|
const dateInLocalTZ = new Date(d.toLocaleString("en-US", { timeZone: "UTC" }));
|
||||||
|
const dateInTargetTZ = new Date(d.toLocaleString("en-US", { timeZone: timeZone || "UTC" }));
|
||||||
|
const tzOffset = dateInTargetTZ.getTime() - dateInLocalTZ.getTime();
|
||||||
|
return new Date(d.getTime() - tzOffset);
|
||||||
|
};
|
||||||
|
|
||||||
export const parseRecurringDates = (
|
export const parseRecurringDates = (
|
||||||
{
|
{
|
||||||
startDate,
|
startDate,
|
||||||
@@ -39,22 +48,16 @@ export const parseRecurringDates = (
|
|||||||
const rule = new RRule({
|
const rule = new RRule({
|
||||||
...restRecurringEvent,
|
...restRecurringEvent,
|
||||||
count: recurringCount,
|
count: recurringCount,
|
||||||
dtstart: new Date(
|
dtstart: dayjs(startDate).utc(true).toDate(),
|
||||||
Date.UTC(
|
});
|
||||||
dayjs.utc(startDate).get("year"),
|
// UTC times with tzOffset applied to account for DST
|
||||||
dayjs.utc(startDate).get("month"),
|
const times = rule.all().map((t) => dateWithZone(t, timeZone));
|
||||||
dayjs.utc(startDate).get("date"),
|
const dateStrings = times.map((t) => {
|
||||||
dayjs.utc(startDate).get("hour"),
|
// undo DST diffs for localized display.
|
||||||
dayjs.utc(startDate).get("minute")
|
return processDate(dayjs.utc(t).tz(timeZone), i18n);
|
||||||
)
|
|
||||||
),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const utcOffset = dayjs(startDate).tz(timeZone).utcOffset();
|
return [dateStrings, times];
|
||||||
const dateStrings = rule.all().map((r) => {
|
|
||||||
return processDate(dayjs.utc(r).utcOffset(utcOffset), i18n);
|
|
||||||
});
|
|
||||||
return [dateStrings, rule.all()];
|
|
||||||
};
|
};
|
||||||
|
|
||||||
export const extractRecurringDates = (
|
export const extractRecurringDates = (
|
||||||
@@ -66,6 +69,7 @@ export const extractRecurringDates = (
|
|||||||
timeZone: string | undefined,
|
timeZone: string | undefined,
|
||||||
i18n: I18n
|
i18n: I18n
|
||||||
): [string[], Date[]] => {
|
): [string[], Date[]] => {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { count = 0, ...rest } =
|
const { count = 0, ...rest } =
|
||||||
booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {};
|
booking.eventType.recurringEvent !== null ? booking.eventType.recurringEvent : {};
|
||||||
const recurringInfo = booking.recurringBookings.find(
|
const recurringInfo = booking.recurringBookings.find(
|
||||||
|
|||||||
@@ -1,59 +0,0 @@
|
|||||||
import { PrismaClient } from "@prisma/client";
|
|
||||||
|
|
||||||
import { TRPCError } from "@calcom/trpc/server";
|
|
||||||
|
|
||||||
import { BASE_URL } from "@lib/config/constants";
|
|
||||||
|
|
||||||
export const samlDatabaseUrl = process.env.SAML_DATABASE_URL || "";
|
|
||||||
export const samlLoginUrl = BASE_URL;
|
|
||||||
|
|
||||||
export const isSAMLLoginEnabled = samlDatabaseUrl.length > 0;
|
|
||||||
|
|
||||||
export const samlTenantID = "Cal.com";
|
|
||||||
export const samlProductID = "Cal.com";
|
|
||||||
|
|
||||||
const samlAdmins = (process.env.SAML_ADMINS || "").split(",");
|
|
||||||
export const hostedCal = BASE_URL === "https://app.cal.com";
|
|
||||||
export const tenantPrefix = "team-";
|
|
||||||
|
|
||||||
export const isSAMLAdmin = (email: string) => {
|
|
||||||
for (const admin of samlAdmins) {
|
|
||||||
if (admin.toLowerCase() === email.toLowerCase() && admin.toUpperCase() === email.toUpperCase()) {
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const samlTenantProduct = async (prisma: PrismaClient, email: string) => {
|
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
where: {
|
|
||||||
email,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
invitedTo: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!user) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "UNAUTHORIZED",
|
|
||||||
message: "Unauthorized Request",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!user.invitedTo) {
|
|
||||||
throw new TRPCError({
|
|
||||||
code: "BAD_REQUEST",
|
|
||||||
message:
|
|
||||||
"Could not find a SAML Identity Provider for your email. Please contact your admin to ensure you have been given access to Cal",
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
tenant: tenantPrefix + user.invitedTo,
|
|
||||||
product: samlProductID,
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -1,5 +1,7 @@
|
|||||||
import { Attendee, Booking } from "@prisma/client";
|
import { Attendee, Booking } from "@prisma/client";
|
||||||
|
|
||||||
|
import { AppsStatus } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
export type BookingCreateBody = {
|
export type BookingCreateBody = {
|
||||||
email: string;
|
email: string;
|
||||||
end: string;
|
end: string;
|
||||||
@@ -33,4 +35,5 @@ export type BookingCreateBody = {
|
|||||||
export type BookingResponse = Booking & {
|
export type BookingResponse = Booking & {
|
||||||
paymentUid?: string;
|
paymentUid?: string;
|
||||||
attendees: Attendee[];
|
attendees: Attendee[];
|
||||||
|
appsStatus?: AppsStatus[];
|
||||||
};
|
};
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { GetServerSideProps, GetServerSidePropsContext, GetServerSidePropsResult } from "next";
|
||||||
|
|
||||||
|
export type EmbedProps = {
|
||||||
|
isEmbed?: boolean;
|
||||||
|
};
|
||||||
|
|
||||||
|
export default function withEmbedSsr(getServerSideProps: GetServerSideProps) {
|
||||||
|
return async (context: GetServerSidePropsContext): Promise<GetServerSidePropsResult<EmbedProps>> => {
|
||||||
|
const ssrResponse = await getServerSideProps(context);
|
||||||
|
if (!("props" in ssrResponse)) {
|
||||||
|
return ssrResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...ssrResponse,
|
||||||
|
props: {
|
||||||
|
...ssrResponse.props,
|
||||||
|
isEmbed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -23,6 +23,13 @@ const middleware: NextMiddleware = async (req) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Ensure that embed query param is there in when /embed is added.
|
||||||
|
// query param is the way in which client side code knows that it is in embed mode.
|
||||||
|
if (url.pathname.endsWith("/embed") && typeof url.searchParams.get("embed") !== "string") {
|
||||||
|
url.searchParams.set("embed", "");
|
||||||
|
return NextResponse.redirect(url);
|
||||||
|
}
|
||||||
|
|
||||||
// Don't 404 old routing_forms links
|
// Don't 404 old routing_forms links
|
||||||
if (url.pathname.startsWith("/apps/routing_forms")) {
|
if (url.pathname.startsWith("/apps/routing_forms")) {
|
||||||
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
url.pathname = url.pathname.replace("/apps/routing_forms", "/apps/routing-forms");
|
||||||
|
|||||||
@@ -134,8 +134,8 @@ const nextConfig = {
|
|||||||
destination: "/api/user/avatar?teamname=:teamname",
|
destination: "/api/user/avatar?teamname=:teamname",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/forms/:formId",
|
source: "/forms/:formQuery*",
|
||||||
destination: "/apps/routing-forms/routing-link/:formId",
|
destination: "/apps/routing-forms/routing-link/:formQuery*",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
source: "/router",
|
source: "/router",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.1.0",
|
"version": "2.1.2",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -23,7 +23,7 @@
|
|||||||
"yarn": ">=1.19.0 < 2.0.0"
|
"yarn": ">=1.19.0 < 2.0.0"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@boxyhq/saml-jackson": "0.3.6",
|
"@boxyhq/saml-jackson": "1.3.1",
|
||||||
"@calcom/app-store": "*",
|
"@calcom/app-store": "*",
|
||||||
"@calcom/app-store-cli": "*",
|
"@calcom/app-store-cli": "*",
|
||||||
"@calcom/core": "*",
|
"@calcom/core": "*",
|
||||||
@@ -60,6 +60,7 @@
|
|||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
"@vercel/edge-functions-ui": "^0.2.1",
|
"@vercel/edge-functions-ui": "^0.2.1",
|
||||||
|
"@vercel/og": "^0.0.19",
|
||||||
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
"@wojtekmaj/react-daterange-picker": "^3.3.1",
|
||||||
"accept-language-parser": "^1.5.0",
|
"accept-language-parser": "^1.5.0",
|
||||||
"async": "^3.2.4",
|
"async": "^3.2.4",
|
||||||
@@ -119,7 +120,7 @@
|
|||||||
"tailwindcss-radix": "^2.6.0",
|
"tailwindcss-radix": "^2.6.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"web3": "^1.7.5",
|
"web3": "^1.7.5",
|
||||||
"zod": "^3.18.0"
|
"zod": "^3.19.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@babel/core": "^7.18.10",
|
"@babel/core": "^7.18.10",
|
||||||
|
|||||||
+20
-17
@@ -13,6 +13,7 @@ import {
|
|||||||
useEmbedStyles,
|
useEmbedStyles,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import EmptyPage from "@calcom/features/eventtypes/components/EmptyPage";
|
||||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||||
import defaultEvents, {
|
import defaultEvents, {
|
||||||
getDynamicEventDescription,
|
getDynamicEventDescription,
|
||||||
@@ -30,6 +31,7 @@ import { BadgeCheckIcon, Icon } from "@calcom/ui/Icon";
|
|||||||
|
|
||||||
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
import { useExposePlanGlobally } from "@lib/hooks/useExposePlanGlobally";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import AvatarGroup from "@components/ui/AvatarGroup";
|
import AvatarGroup from "@components/ui/AvatarGroup";
|
||||||
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
import { AvatarSSR } from "@components/ui/AvatarSSR";
|
||||||
@@ -38,7 +40,7 @@ import { ssrInit } from "@server/lib/ssr";
|
|||||||
|
|
||||||
const EventTypeDescription = dynamic(() => import("@calcom/ui/v2/modules/event-types/EventTypeDescription"));
|
const EventTypeDescription = dynamic(() => import("@calcom/ui/v2/modules/event-types/EventTypeDescription"));
|
||||||
const HeadSeo = dynamic(() => import("@components/seo/head-seo"));
|
const HeadSeo = dynamic(() => import("@components/seo/head-seo"));
|
||||||
export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||||
const { users, profile, eventTypes, isDynamicGroup, dynamicNames, dynamicUsernames, isSingleUser } = props;
|
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
|
const [user] = users; //To be used when we only have a single user, not dynamic group
|
||||||
useTheme(user.theme);
|
useTheme(user.theme);
|
||||||
@@ -86,7 +88,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
</ul>
|
</ul>
|
||||||
);
|
);
|
||||||
|
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed(props.isEmbed);
|
||||||
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
const eventTypeListItemEmbedStyles = useEmbedStyles("eventTypeListItem");
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
const shouldAlignCentrally = !isEmbed || shouldAlignCentrallyInEmbed;
|
||||||
@@ -102,7 +104,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
||||||
}
|
}
|
||||||
}, [telemetry, router.asPath]);
|
}, [telemetry, router.asPath]);
|
||||||
|
const isEventListEmpty = eventTypes.length === 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<HeadSeo
|
<HeadSeo
|
||||||
@@ -110,9 +112,13 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
description={
|
description={
|
||||||
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
|
isDynamicGroup ? `Book events with ${dynamicUsernames.join(", ")}` : (user.bio as string) || ""
|
||||||
}
|
}
|
||||||
name={isDynamicGroup ? dynamicNames.join(", ") : nameOrUsername}
|
meeting={{
|
||||||
username={isDynamicGroup ? dynamicUsernames.join(", ") : (user.username as string) || ""}
|
title: isDynamicGroup ? "" : `${user.bio}`,
|
||||||
// avatar={user.avatar || undefined}
|
profile: { name: `${profile.name}`, image: null },
|
||||||
|
users: isDynamicGroup
|
||||||
|
? dynamicUsernames.map((username, index) => ({ username, name: dynamicNames[index] }))
|
||||||
|
: [{ username: `${user.username}`, name: `${user.name}` }],
|
||||||
|
}}
|
||||||
/>
|
/>
|
||||||
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
<CustomBranding lightVal={profile.brandColor} darkVal={profile.darkBrandColor} />
|
||||||
|
|
||||||
@@ -143,7 +149,11 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div
|
<div
|
||||||
className="rounded-md border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
|
className={classNames(
|
||||||
|
"rounded-md ",
|
||||||
|
!isEventListEmpty &&
|
||||||
|
"border border-neutral-200 dark:border-neutral-700 dark:hover:border-neutral-600"
|
||||||
|
)}
|
||||||
data-testid="event-types">
|
data-testid="event-types">
|
||||||
{user.away ? (
|
{user.away ? (
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
||||||
@@ -190,16 +200,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
))
|
))
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
{eventTypes.length === 0 && (
|
{isEventListEmpty && <EmptyPage name={user.name ?? "User"} />}
|
||||||
<div className="overflow-hidden rounded-sm border dark:border-gray-900">
|
|
||||||
<div className="p-8 text-center text-gray-400 dark:text-white">
|
|
||||||
<h2 className="font-cal mb-2 text-3xl text-gray-600 dark:text-white">
|
|
||||||
{t("uh_oh") as string}
|
|
||||||
</h2>
|
|
||||||
<p className="mx-auto max-w-md">{t("no_event_types_have_been_setup") as string}</p>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</main>
|
</main>
|
||||||
<Toaster position="bottom-right" />
|
<Toaster position="bottom-right" />
|
||||||
</div>
|
</div>
|
||||||
@@ -283,6 +284,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
if (!users.length) {
|
if (!users.length) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
const isDynamicGroup = users.length > 1;
|
const isDynamicGroup = users.length > 1;
|
||||||
|
|||||||
@@ -8,13 +8,15 @@ import { getDefaultEvent, getGroupName, getUsernameList } from "@calcom/lib/defa
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
import { parseRecurringEvent } from "@calcom/lib/isRecurringEvent";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
import { User } from "@calcom/prisma/client";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps>;
|
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
||||||
|
|
||||||
export default function Type(props: AvailabilityPageProps) {
|
export default function Type(props: AvailabilityPageProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -77,8 +79,27 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
brandColor: true,
|
brandColor: true,
|
||||||
darkBrandColor: true,
|
darkBrandColor: true,
|
||||||
eventTypes: {
|
eventTypes: {
|
||||||
select: { id: true },
|
where: {
|
||||||
// Order by position is important to ensure that the event-type that's enabled is the first in the list because for Free user only first is allowed.
|
// Many-to-many relationship causes inclusion of the team events - cool -
|
||||||
|
// but to prevent these from being selected, make sure the teamId is NULL.
|
||||||
|
AND: [{ slug }, { teamId: null }],
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
title: true,
|
||||||
|
slug: true,
|
||||||
|
hidden: true,
|
||||||
|
recurringEvent: true,
|
||||||
|
length: true,
|
||||||
|
locations: true,
|
||||||
|
id: true,
|
||||||
|
description: true,
|
||||||
|
price: true,
|
||||||
|
currency: true,
|
||||||
|
requiresConfirmation: true,
|
||||||
|
schedulingType: true,
|
||||||
|
metadata: true,
|
||||||
|
seatsPerTimeSlot: true,
|
||||||
|
},
|
||||||
orderBy: [
|
orderBy: [
|
||||||
{
|
{
|
||||||
position: "desc",
|
position: "desc",
|
||||||
@@ -91,57 +112,24 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return { notFound: true };
|
if (!user || !user.eventTypes) return { notFound: true };
|
||||||
|
|
||||||
const eventTypes = await prisma.eventType.findMany({
|
const [eventType]: (typeof user.eventTypes[number] & {
|
||||||
where: {
|
users: Pick<User, "name" | "username" | "hideBranding" | "plan" | "timeZone">[];
|
||||||
slug,
|
})[] = [
|
||||||
OR: [{ userId: user.id }, { users: { some: { id: user.id } } }],
|
{
|
||||||
},
|
...user.eventTypes[0],
|
||||||
// Order is important to ensure that given a slug if there are duplicates, we choose the same event type consistently when showing in event-types list UI(in terms of ordering and disabled event types)
|
users: [
|
||||||
// TODO: If we can ensure that there are no duplicates for a [slug, userId] combination in existing data, this requirement might be avoided.
|
{
|
||||||
orderBy: [
|
name: user.name,
|
||||||
{
|
username: user.username,
|
||||||
position: "desc",
|
hideBranding: user.hideBranding,
|
||||||
},
|
plan: user.plan,
|
||||||
{
|
timeZone: user.timeZone,
|
||||||
id: "asc",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
slug: true,
|
|
||||||
hidden: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
length: true,
|
|
||||||
locations: true,
|
|
||||||
id: true,
|
|
||||||
description: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
requiresConfirmation: true,
|
|
||||||
schedulingType: true,
|
|
||||||
metadata: true,
|
|
||||||
seatsPerTimeSlot: true,
|
|
||||||
users: {
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
hideBranding: true,
|
|
||||||
brandColor: true,
|
|
||||||
darkBrandColor: true,
|
|
||||||
theme: true,
|
|
||||||
plan: true,
|
|
||||||
allowDynamicBooking: true,
|
|
||||||
timeZone: true,
|
|
||||||
},
|
},
|
||||||
},
|
],
|
||||||
},
|
},
|
||||||
});
|
];
|
||||||
|
|
||||||
if (!eventTypes) return { notFound: true };
|
|
||||||
|
|
||||||
const [eventType] = eventTypes;
|
|
||||||
|
|
||||||
if (!eventType) return { notFound: true };
|
if (!eventType) return { notFound: true };
|
||||||
|
|
||||||
@@ -151,33 +139,20 @@ async function getUserPageProps(context: GetStaticPropsContext) {
|
|||||||
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
metadata: EventTypeMetaDataSchema.parse(eventType.metadata || {}),
|
||||||
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(eventType.recurringEvent),
|
||||||
locations: privacyFilteredLocations(locations),
|
locations: privacyFilteredLocations(locations),
|
||||||
users: eventType.users.map((user) => ({
|
|
||||||
name: user.name,
|
|
||||||
username: user.username,
|
|
||||||
hideBranding: user.hideBranding,
|
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
|
||||||
})),
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const profile = eventType.users[0] || user;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
eventType: eventTypeObject,
|
eventType: eventTypeObject,
|
||||||
profile: {
|
profile: {
|
||||||
|
...eventType.users[0],
|
||||||
theme: user.theme,
|
theme: user.theme,
|
||||||
name: user.name,
|
|
||||||
username: user.username,
|
|
||||||
hideBranding: user.hideBranding,
|
|
||||||
plan: user.plan,
|
|
||||||
timeZone: user.timeZone,
|
|
||||||
allowDynamicBooking: false,
|
allowDynamicBooking: false,
|
||||||
weekStart: user.weekStart,
|
weekStart: user.weekStart,
|
||||||
brandColor: user.brandColor,
|
brandColor: user.brandColor,
|
||||||
darkBrandColor: user.darkBrandColor,
|
darkBrandColor: user.darkBrandColor,
|
||||||
slug: `${profile.username}/${eventType.slug}`,
|
slug: `${user.username}/${eventType.slug}`,
|
||||||
image: `${WEBAPP_URL}/${profile.username}/avatar.png`,
|
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
},
|
},
|
||||||
away: user?.away,
|
away: user?.away,
|
||||||
isDynamic: false,
|
isDynamic: false,
|
||||||
|
|||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import { GetStaticPropsContext } from "next";
|
||||||
|
|
||||||
|
import { getStaticProps as _getStaticProps } from "../[type]";
|
||||||
|
|
||||||
|
export { getStaticPaths } from "../[type]";
|
||||||
|
|
||||||
|
export { default } from "../[type]";
|
||||||
|
|
||||||
|
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||||
|
const staticResponse = await _getStaticProps(context);
|
||||||
|
if (staticResponse.notFound) {
|
||||||
|
return staticResponse;
|
||||||
|
}
|
||||||
|
return {
|
||||||
|
...staticResponse,
|
||||||
|
props: {
|
||||||
|
...staticResponse.props,
|
||||||
|
isEmbed: true,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -197,6 +197,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
isDynamicGroupBooking,
|
isDynamicGroupBooking,
|
||||||
hasHashedBookingLink: false,
|
hasHashedBookingLink: false,
|
||||||
hashedLink: null,
|
hashedLink: null,
|
||||||
|
isEmbed: typeof context.query.embed === "string",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[user]";
|
||||||
|
|
||||||
|
export { default } from "../[user]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -3,18 +3,13 @@ import Document, { DocumentContext, Head, Html, Main, NextScript, DocumentProps
|
|||||||
type Props = Record<string, unknown> & DocumentProps;
|
type Props = Record<string, unknown> & DocumentProps;
|
||||||
|
|
||||||
function toRunBeforeReactOnClient() {
|
function toRunBeforeReactOnClient() {
|
||||||
const calEmbedMode = location.search.includes("embed=");
|
const calEmbedMode =
|
||||||
try {
|
location.search.includes("embed=") ||
|
||||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
/* Iframe Name */
|
||||||
window.sessionStorage.setItem("calEmbedMode", String(calEmbedMode));
|
window.name.includes("cal-embed");
|
||||||
} catch (e) {}
|
|
||||||
|
|
||||||
window.isEmbed = () => {
|
window.isEmbed = () => {
|
||||||
try {
|
// Once an embed mode always an embed mode
|
||||||
// eslint-disable-next-line @calcom/eslint/avoid-web-storage
|
|
||||||
return window.sessionStorage.getItem("calEmbedMode") === "true";
|
|
||||||
} catch (e) {}
|
|
||||||
// If we can't use sessionStorage to retrieve embed mode, just use the variable. It would fail to detect embed if page in iframe reloads without embed query param in it.
|
|
||||||
return calEmbedMode;
|
return calEmbedMode;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -45,14 +40,14 @@ function toRunBeforeReactOnClient() {
|
|||||||
|
|
||||||
class MyDocument extends Document<Props> {
|
class MyDocument extends Document<Props> {
|
||||||
static async getInitialProps(ctx: DocumentContext) {
|
static async getInitialProps(ctx: DocumentContext) {
|
||||||
|
const isEmbed = ctx.asPath?.includes("/embed") || ctx.asPath?.includes("embedType=");
|
||||||
const initialProps = await Document.getInitialProps(ctx);
|
const initialProps = await Document.getInitialProps(ctx);
|
||||||
return { ...initialProps };
|
return { isEmbed, ...initialProps };
|
||||||
}
|
}
|
||||||
|
|
||||||
render() {
|
render() {
|
||||||
const { locale } = this.props.__NEXT_DATA__;
|
const { locale } = this.props.__NEXT_DATA__;
|
||||||
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
|
const dir = locale === "ar" || locale === "he" ? "rtl" : "ltr";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Html lang={locale} dir={dir}>
|
<Html lang={locale} dir={dir}>
|
||||||
<Head>
|
<Head>
|
||||||
@@ -82,22 +77,22 @@ class MyDocument extends Document<Props> {
|
|||||||
/>
|
/>
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<body className="dark:bg-darkgray-50 desktop-transparent bg-gray-100">
|
<body
|
||||||
|
className="dark:bg-darkgray-50 desktop-transparent bg-gray-100"
|
||||||
|
style={
|
||||||
|
this.props.isEmbed
|
||||||
|
? {
|
||||||
|
background: "transparent",
|
||||||
|
// Keep the embed hidden till parent initializes and
|
||||||
|
// - gives it the appropriate styles if UI instruction is there.
|
||||||
|
// - gives iframe the appropriate height(equal to document height) which can only be known after loading the page once in browser.
|
||||||
|
// - Tells iframe which mode it should be in (dark/light) - if there is a a UI instruction for that
|
||||||
|
visibility: "hidden",
|
||||||
|
}
|
||||||
|
: {}
|
||||||
|
}>
|
||||||
<Main />
|
<Main />
|
||||||
<NextScript />
|
<NextScript />
|
||||||
{/* In case of Embed we want background to be transparent so that it merges into the website seamlessly. Also, we keep the body hidden here and embed logic would take care of showing the body when it's ready */}
|
|
||||||
{/* We are doing it on browser and not on server because there are some pages which are not SSRd */}
|
|
||||||
<script
|
|
||||||
dangerouslySetInnerHTML={{
|
|
||||||
__html: `
|
|
||||||
if (isEmbed()) {
|
|
||||||
if(!isPageOptimizedForEmbed()) {
|
|
||||||
document.body.style.display="none";
|
|
||||||
}
|
|
||||||
document.body.style.background="transparent";
|
|
||||||
}`,
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</body>
|
</body>
|
||||||
</Html>
|
</Html>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import path from "path";
|
|||||||
|
|
||||||
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
import checkLicense from "@calcom/features/ee/common/server/checkLicense";
|
||||||
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
import ImpersonationProvider from "@calcom/features/ee/impersonation/lib/ImpersonationProvider";
|
||||||
|
import { hostedCal, isSAMLLoginEnabled } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
import { symmetricDecrypt } from "@calcom/lib/crypto";
|
||||||
import { defaultCookies } from "@calcom/lib/default-cookies";
|
import { defaultCookies } from "@calcom/lib/default-cookies";
|
||||||
@@ -22,7 +23,6 @@ import prisma from "@calcom/prisma";
|
|||||||
import { ErrorCode, verifyPassword } from "@lib/auth";
|
import { ErrorCode, verifyPassword } from "@lib/auth";
|
||||||
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
import CalComAdapter from "@lib/auth/next-auth-custom-adapter";
|
||||||
import { randomString } from "@lib/random";
|
import { randomString } from "@lib/random";
|
||||||
import { hostedCal, isSAMLLoginEnabled, samlLoginUrl } from "@lib/saml";
|
|
||||||
import slugify from "@lib/slugify";
|
import slugify from "@lib/slugify";
|
||||||
|
|
||||||
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
import { GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, IS_GOOGLE_LOGIN_ENABLED } from "@server/lib/constants";
|
||||||
@@ -135,7 +135,7 @@ if (isSAMLLoginEnabled) {
|
|||||||
version: "2.0",
|
version: "2.0",
|
||||||
checks: ["pkce", "state"],
|
checks: ["pkce", "state"],
|
||||||
authorization: {
|
authorization: {
|
||||||
url: `${samlLoginUrl}/api/auth/saml/authorize`,
|
url: `${WEBAPP_URL}/api/auth/saml/authorize`,
|
||||||
params: {
|
params: {
|
||||||
scope: "",
|
scope: "",
|
||||||
response_type: "code",
|
response_type: "code",
|
||||||
@@ -143,10 +143,10 @@ if (isSAMLLoginEnabled) {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
token: {
|
token: {
|
||||||
url: `${samlLoginUrl}/api/auth/saml/token`,
|
url: `${WEBAPP_URL}/api/auth/saml/token`,
|
||||||
params: { grant_type: "authorization_code" },
|
params: { grant_type: "authorization_code" },
|
||||||
},
|
},
|
||||||
userinfo: `${samlLoginUrl}/api/auth/saml/userinfo`,
|
userinfo: `${WEBAPP_URL}/api/auth/saml/userinfo`,
|
||||||
profile: (profile) => {
|
profile: (profile) => {
|
||||||
return {
|
return {
|
||||||
id: profile.id || "",
|
id: profile.id || "",
|
||||||
|
|||||||
@@ -1,25 +1,24 @@
|
|||||||
import { OAuthReqBody } from "@boxyhq/saml-jackson";
|
import { OAuthReq } from "@boxyhq/saml-jackson";
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
|
||||||
import jackson from "@lib/jackson";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
try {
|
const { oauthController } = await jackson();
|
||||||
if (req.method !== "GET") {
|
|
||||||
throw new Error("Method not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { oauthController } = await jackson();
|
if (req.method !== "GET") {
|
||||||
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReqBody);
|
return res.status(400).send("Method not allowed");
|
||||||
res.redirect(302, redirect_url);
|
}
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof HttpError) {
|
try {
|
||||||
console.error("authorize error:", err);
|
const { redirect_url } = await oauthController.authorize(req.query as unknown as OAuthReq);
|
||||||
const { message, statusCode = 500 } = err;
|
|
||||||
return res.status(statusCode).send(message);
|
return res.redirect(302, redirect_url as string);
|
||||||
}
|
} catch (err) {
|
||||||
return res.status(500).send("Unknown error");
|
const { message, statusCode = 500 } = err as HttpError;
|
||||||
|
|
||||||
|
return res.status(statusCode).send(message);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1,25 +1,14 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
import jackson from "@lib/jackson";
|
async function postHandler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
const { oauthController } = await jackson();
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
const { redirect_url } = await oauthController.samlResponse(req.body);
|
||||||
try {
|
if (redirect_url) return res.redirect(302, redirect_url);
|
||||||
if (req.method !== "POST") {
|
|
||||||
throw new Error("Method not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { oauthController } = await jackson();
|
|
||||||
const { redirect_url } = await oauthController.samlResponse(req.body);
|
|
||||||
|
|
||||||
res.redirect(302, redirect_url);
|
|
||||||
} catch (err: unknown) {
|
|
||||||
if (err instanceof HttpError) {
|
|
||||||
console.error("callback error:", err);
|
|
||||||
const { message, statusCode = 500 } = err;
|
|
||||||
return res.status(statusCode).send(message);
|
|
||||||
}
|
|
||||||
return res.status(500).send("Unknown error");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||||
|
});
|
||||||
@@ -1,21 +1,13 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest } from "next";
|
||||||
|
|
||||||
import jackson from "@lib/jackson";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function postHandler(req: NextApiRequest) {
|
||||||
try {
|
const { oauthController } = await jackson();
|
||||||
if (req.method !== "POST") {
|
return await oauthController.token(req.body);
|
||||||
throw new Error("Method not allowed");
|
|
||||||
}
|
|
||||||
|
|
||||||
const { oauthController } = await jackson();
|
|
||||||
const result = await oauthController.token(req.body);
|
|
||||||
|
|
||||||
res.json(result);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("token error:", err);
|
|
||||||
const { message, statusCode = 500 } = err;
|
|
||||||
|
|
||||||
res.status(statusCode).send(message);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: defaultResponder(postHandler) }),
|
||||||
|
});
|
||||||
@@ -1,53 +1,34 @@
|
|||||||
import { NextApiRequest, NextApiResponse } from "next";
|
import { NextApiRequest } from "next";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import jackson from "@lib/jackson";
|
import jackson from "@calcom/features/ee/sso/lib/jackson";
|
||||||
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
||||||
|
|
||||||
const extractAuthToken = (req: NextApiRequest) => {
|
const extractAuthToken = (req: NextApiRequest) => {
|
||||||
const authHeader = req.headers["authorization"];
|
const authHeader = req.headers["authorization"];
|
||||||
const parts = (authHeader || "").split(" ");
|
const parts = (authHeader || "").split(" ");
|
||||||
if (parts.length > 1) {
|
if (parts.length > 1) return parts[1];
|
||||||
return parts[1];
|
|
||||||
}
|
|
||||||
|
|
||||||
return null;
|
// check for query param
|
||||||
|
let arr: string[] = [];
|
||||||
|
const { access_token } = requestQuery.parse(req.query);
|
||||||
|
arr = arr.concat(access_token);
|
||||||
|
if (arr[0].length > 0) return arr[0];
|
||||||
|
|
||||||
|
throw new HttpError({ statusCode: 401, message: "Unauthorized" });
|
||||||
};
|
};
|
||||||
|
|
||||||
const requestQuery = z.object({
|
const requestQuery = z.object({
|
||||||
access_token: z.string(),
|
access_token: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
async function getHandler(req: NextApiRequest) {
|
||||||
try {
|
const { oauthController } = await jackson();
|
||||||
if (req.method !== "GET") {
|
const token = extractAuthToken(req);
|
||||||
throw new Error("Method not allowed");
|
return await oauthController.userInfo(token);
|
||||||
}
|
|
||||||
|
|
||||||
const { oauthController } = await jackson();
|
|
||||||
let token: string | null = extractAuthToken(req);
|
|
||||||
|
|
||||||
// check for query param
|
|
||||||
if (!token) {
|
|
||||||
let arr: string[] = [];
|
|
||||||
const { access_token } = requestQuery.parse(req.query);
|
|
||||||
arr = arr.concat(access_token);
|
|
||||||
if (arr[0].length > 0) {
|
|
||||||
token = arr[0];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!token) {
|
|
||||||
res.status(401).json({ message: "Unauthorized" });
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const profile = await oauthController.userInfo(token);
|
|
||||||
|
|
||||||
res.json(profile);
|
|
||||||
} catch (err: any) {
|
|
||||||
console.error("userinfo error:", err);
|
|
||||||
const { message, statusCode = 500 } = err;
|
|
||||||
|
|
||||||
res.status(statusCode).json({ message });
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
GET: Promise.resolve({ default: defaultResponder(getHandler) }),
|
||||||
|
});
|
||||||
@@ -1,294 +0,0 @@
|
|||||||
import {
|
|
||||||
Attendee,
|
|
||||||
Booking,
|
|
||||||
BookingReference,
|
|
||||||
BookingStatus,
|
|
||||||
EventType,
|
|
||||||
User,
|
|
||||||
WebhookTriggerEvents,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import { getSession } from "next-auth/react";
|
|
||||||
import type { TFunction } from "next-i18next";
|
|
||||||
import { z, ZodError } from "zod";
|
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
|
||||||
import { CalendarEventBuilder } from "@calcom/core/builders/CalendarEvent/builder";
|
|
||||||
import { CalendarEventDirector } from "@calcom/core/builders/CalendarEvent/director";
|
|
||||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import { sendRequestRescheduleEmail } from "@calcom/emails";
|
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|
||||||
import sendPayload from "@calcom/features/webhooks/lib/sendPayload";
|
|
||||||
import { isPrismaObjOrUndefined } from "@calcom/lib";
|
|
||||||
import { getTranslation } from "@calcom/lib/server/i18n";
|
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import type { CalendarEvent, Person } from "@calcom/types/Calendar";
|
|
||||||
|
|
||||||
export type RescheduleResponse = Booking & {
|
|
||||||
attendees: Attendee[];
|
|
||||||
};
|
|
||||||
export type PersonAttendeeCommonFields = Pick<
|
|
||||||
User,
|
|
||||||
"id" | "email" | "name" | "locale" | "timeZone" | "username"
|
|
||||||
>;
|
|
||||||
|
|
||||||
const rescheduleSchema = z.object({
|
|
||||||
bookingId: z.string(),
|
|
||||||
rescheduleReason: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
const findUserDataByUserId = async (userId: number) => {
|
|
||||||
return await prisma.user.findUniqueOrThrow({
|
|
||||||
where: {
|
|
||||||
id: userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
name: true,
|
|
||||||
username: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
locale: true,
|
|
||||||
credentials: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
teams: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const handler = async (
|
|
||||||
req: NextApiRequest,
|
|
||||||
res: NextApiResponse
|
|
||||||
): Promise<RescheduleResponse | NextApiResponse | void> => {
|
|
||||||
const session = await getSession({ req });
|
|
||||||
const {
|
|
||||||
bookingId,
|
|
||||||
rescheduleReason: cancellationReason,
|
|
||||||
}: { bookingId: string; rescheduleReason: string; cancellationReason: string } = req.body;
|
|
||||||
let user: Awaited<ReturnType<typeof findUserDataByUserId>>;
|
|
||||||
try {
|
|
||||||
if (session?.user?.id) {
|
|
||||||
user = await findUserDataByUserId(session?.user.id);
|
|
||||||
} else {
|
|
||||||
return res.status(501).end();
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingToReschedule = await prisma.booking.findFirstOrThrow({
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
uid: true,
|
|
||||||
userId: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
startTime: true,
|
|
||||||
endTime: true,
|
|
||||||
eventTypeId: true,
|
|
||||||
eventType: true,
|
|
||||||
location: true,
|
|
||||||
attendees: true,
|
|
||||||
references: true,
|
|
||||||
customInputs: true,
|
|
||||||
dynamicEventSlugRef: true,
|
|
||||||
dynamicGroupSlugRef: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
uid: bookingId,
|
|
||||||
NOT: {
|
|
||||||
status: {
|
|
||||||
in: [BookingStatus.CANCELLED, BookingStatus.REJECTED],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookingToReschedule.userId) {
|
|
||||||
throw new Error("Booking to reschedule doesn't have an owner.");
|
|
||||||
}
|
|
||||||
if (!bookingToReschedule.eventType) {
|
|
||||||
throw new Error("EventType not found for current booking.");
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingBelongsToTeam = !!bookingToReschedule.eventType?.teamId;
|
|
||||||
if (bookingBelongsToTeam && bookingToReschedule.eventType?.teamId) {
|
|
||||||
const userTeamIds = user.teams.map((item) => item.teamId);
|
|
||||||
if (userTeamIds.indexOf(bookingToReschedule?.eventType?.teamId) === -1) {
|
|
||||||
throw new Error("User isn't a member on the team");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (!bookingBelongsToTeam && bookingToReschedule.userId !== user.id) {
|
|
||||||
throw new Error("User isn't owner of the current booking");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bookingToReschedule && user) {
|
|
||||||
let event: Partial<EventType> = {};
|
|
||||||
if (bookingToReschedule.eventTypeId) {
|
|
||||||
event = await prisma.eventType.findFirstOrThrow({
|
|
||||||
select: {
|
|
||||||
title: true,
|
|
||||||
users: true,
|
|
||||||
schedulingType: true,
|
|
||||||
recurringEvent: true,
|
|
||||||
},
|
|
||||||
where: {
|
|
||||||
id: bookingToReschedule.eventTypeId,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingToReschedule.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
rescheduled: true,
|
|
||||||
cancellationReason,
|
|
||||||
status: BookingStatus.CANCELLED,
|
|
||||||
updatedAt: dayjs().toISOString(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const [mainAttendee] = bookingToReschedule.attendees;
|
|
||||||
// @NOTE: Should we assume attendees language?
|
|
||||||
const tAttendees = await getTranslation(mainAttendee.locale ?? "en", "common");
|
|
||||||
const usersToPeopleType = (
|
|
||||||
users: PersonAttendeeCommonFields[],
|
|
||||||
selectedLanguage: TFunction
|
|
||||||
): Person[] => {
|
|
||||||
return users?.map((user) => {
|
|
||||||
return {
|
|
||||||
email: user.email || "",
|
|
||||||
name: user.name || "",
|
|
||||||
username: user?.username || "",
|
|
||||||
language: { translate: selectedLanguage, locale: user.locale || "en" },
|
|
||||||
timeZone: user?.timeZone,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const userTranslation = await getTranslation(user.locale ?? "en", "common");
|
|
||||||
const [userAsPeopleType] = usersToPeopleType([user], userTranslation);
|
|
||||||
|
|
||||||
const builder = new CalendarEventBuilder();
|
|
||||||
builder.init({
|
|
||||||
title: bookingToReschedule.title,
|
|
||||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
|
||||||
startTime: bookingToReschedule.startTime.toISOString(),
|
|
||||||
endTime: bookingToReschedule.endTime.toISOString(),
|
|
||||||
attendees: usersToPeopleType(
|
|
||||||
// username field doesn't exists on attendee but could be in the future
|
|
||||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
|
||||||
tAttendees
|
|
||||||
),
|
|
||||||
organizer: userAsPeopleType,
|
|
||||||
});
|
|
||||||
|
|
||||||
const director = new CalendarEventDirector();
|
|
||||||
director.setBuilder(builder);
|
|
||||||
director.setExistingBooking(bookingToReschedule);
|
|
||||||
director.setCancellationReason(cancellationReason);
|
|
||||||
if (event) {
|
|
||||||
await director.buildForRescheduleEmail();
|
|
||||||
} else {
|
|
||||||
await director.buildWithoutEventTypeForRescheduleEmail();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handling calendar and videos cancellation
|
|
||||||
// This can set previous time as available, until virtual calendar is done
|
|
||||||
const credentialsMap = new Map();
|
|
||||||
user.credentials.forEach((credential) => {
|
|
||||||
credentialsMap.set(credential.type, credential);
|
|
||||||
});
|
|
||||||
const bookingRefsFiltered: BookingReference[] = bookingToReschedule.references.filter(
|
|
||||||
(ref) => !!credentialsMap.get(ref.type)
|
|
||||||
);
|
|
||||||
bookingRefsFiltered.forEach((bookingRef) => {
|
|
||||||
if (bookingRef.uid) {
|
|
||||||
if (bookingRef.type.endsWith("_calendar")) {
|
|
||||||
const calendar = getCalendar(credentialsMap.get(bookingRef.type));
|
|
||||||
|
|
||||||
return calendar?.deleteEvent(
|
|
||||||
bookingRef.uid,
|
|
||||||
builder.calendarEvent,
|
|
||||||
bookingRef.externalCalendarId
|
|
||||||
);
|
|
||||||
} else if (bookingRef.type.endsWith("_video")) {
|
|
||||||
return deleteMeeting(credentialsMap.get(bookingRef.type), bookingRef.uid);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// Send emails
|
|
||||||
await sendRequestRescheduleEmail(builder.calendarEvent, {
|
|
||||||
rescheduleLink: builder.rescheduleLink,
|
|
||||||
});
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
title: bookingToReschedule?.title,
|
|
||||||
type: event && event.title ? event.title : bookingToReschedule.title,
|
|
||||||
description: bookingToReschedule?.description || "",
|
|
||||||
customInputs: isPrismaObjOrUndefined(bookingToReschedule.customInputs),
|
|
||||||
startTime: bookingToReschedule?.startTime ? dayjs(bookingToReschedule.startTime).format() : "",
|
|
||||||
endTime: bookingToReschedule?.endTime ? dayjs(bookingToReschedule.endTime).format() : "",
|
|
||||||
organizer: userAsPeopleType,
|
|
||||||
attendees: usersToPeopleType(
|
|
||||||
// username field doesn't exists on attendee but could be in the future
|
|
||||||
bookingToReschedule.attendees as unknown as PersonAttendeeCommonFields[],
|
|
||||||
tAttendees
|
|
||||||
),
|
|
||||||
uid: bookingToReschedule?.uid,
|
|
||||||
location: bookingToReschedule?.location,
|
|
||||||
destinationCalendar:
|
|
||||||
bookingToReschedule?.destinationCalendar || bookingToReschedule?.destinationCalendar,
|
|
||||||
cancellationReason: `Please reschedule. ${cancellationReason}`, // TODO::Add i18-next for this
|
|
||||||
};
|
|
||||||
|
|
||||||
// Send webhook
|
|
||||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
|
||||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
|
||||||
const subscriberOptions = {
|
|
||||||
userId: bookingToReschedule.userId,
|
|
||||||
eventTypeId: (bookingToReschedule.eventTypeId as number) || 0,
|
|
||||||
triggerEvent: eventTrigger,
|
|
||||||
};
|
|
||||||
const webhooks = await getWebhooks(subscriberOptions);
|
|
||||||
const promises = webhooks.map((webhook) =>
|
|
||||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, evt).catch((e) => {
|
|
||||||
console.error(
|
|
||||||
`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`,
|
|
||||||
e
|
|
||||||
);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
}
|
|
||||||
|
|
||||||
return res.status(200).json(bookingToReschedule);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof Error) {
|
|
||||||
throw new Error("Error.request.reschedule " + error?.message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
function validate(
|
|
||||||
handler: (req: NextApiRequest, res: NextApiResponse) => Promise<RescheduleResponse | NextApiResponse | void>
|
|
||||||
) {
|
|
||||||
return async (req: NextApiRequest, res: NextApiResponse) => {
|
|
||||||
if (req.method === "POST") {
|
|
||||||
try {
|
|
||||||
rescheduleSchema.parse(req.body);
|
|
||||||
} catch (error) {
|
|
||||||
if (error instanceof ZodError && error?.name === "ZodError") {
|
|
||||||
return res.status(400).json(error?.issues);
|
|
||||||
}
|
|
||||||
return res.status(402).end();
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return res.status(405).end();
|
|
||||||
}
|
|
||||||
await handler(req, res);
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
export default validate(handler);
|
|
||||||
@@ -1,396 +1,14 @@
|
|||||||
import {
|
import type { NextApiRequest } from "next";
|
||||||
BookingStatus,
|
|
||||||
Prisma,
|
|
||||||
PrismaPromise,
|
|
||||||
WebhookTriggerEvents,
|
|
||||||
WorkflowMethods,
|
|
||||||
WorkflowReminder,
|
|
||||||
} from "@prisma/client";
|
|
||||||
import { NextApiRequest, NextApiResponse } from "next";
|
|
||||||
import z from "zod";
|
|
||||||
|
|
||||||
import { getCalendar } from "@calcom/app-store/_utils/getCalendar";
|
import handleCancelBooking from "@calcom/features/bookings/lib/handleCancelBooking";
|
||||||
import { FAKE_DAILY_CREDENTIAL } from "@calcom/app-store/dailyvideo/lib/VideoApiAdapter";
|
|
||||||
import { DailyLocationType } from "@calcom/app-store/locations";
|
|
||||||
import { refund } from "@calcom/app-store/stripepayment/lib/server";
|
|
||||||
import { cancelScheduledJobs } from "@calcom/app-store/zapier/lib/nodeScheduler";
|
|
||||||
import { deleteMeeting } from "@calcom/core/videoClient";
|
|
||||||
import dayjs from "@calcom/dayjs";
|
|
||||||
import { sendCancelledEmails } from "@calcom/emails";
|
|
||||||
import { deleteScheduledEmailReminder } from "@calcom/features/ee/workflows/lib/reminders/emailReminderManager";
|
|
||||||
import { sendCancelledReminders } from "@calcom/features/ee/workflows/lib/reminders/reminderScheduler";
|
|
||||||
import { deleteScheduledSMSReminder } from "@calcom/features/ee/workflows/lib/reminders/smsReminderManager";
|
|
||||||
import getWebhooks from "@calcom/features/webhooks/lib/getWebhooks";
|
|
||||||
import sendPayload, { EventTypeInfo } from "@calcom/features/webhooks/lib/sendPayload";
|
|
||||||
import { isPrismaObjOrUndefined, parseRecurringEvent } from "@calcom/lib";
|
|
||||||
import { getSession } from "@calcom/lib/auth";
|
import { getSession } from "@calcom/lib/auth";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { defaultResponder, defaultHandler } from "@calcom/lib/server";
|
||||||
import { defaultHandler, defaultResponder } from "@calcom/lib/server";
|
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
|
||||||
import type { CalendarEvent } from "@calcom/types/Calendar";
|
|
||||||
|
|
||||||
import { getTranslation } from "@server/lib/i18n";
|
async function handler(req: NextApiRequest & { userId?: number }) {
|
||||||
|
|
||||||
const bodySchema = z.object({
|
|
||||||
uid: z.string(),
|
|
||||||
allRemainingBookings: z.boolean().optional(),
|
|
||||||
cancellationReason: z.string().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|
||||||
const { uid, allRemainingBookings, cancellationReason } = bodySchema.parse(req.body);
|
|
||||||
const session = await getSession({ req });
|
const session = await getSession({ req });
|
||||||
|
/* To mimic API behavior */
|
||||||
const bookingToDelete = await prisma.booking.findUnique({
|
req.userId = session?.user?.id;
|
||||||
where: {
|
return await handleCancelBooking(req);
|
||||||
uid,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
...bookingMinimalSelect,
|
|
||||||
recurringEventId: true,
|
|
||||||
userId: true,
|
|
||||||
user: {
|
|
||||||
select: {
|
|
||||||
id: true,
|
|
||||||
credentials: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
name: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
location: true,
|
|
||||||
references: {
|
|
||||||
select: {
|
|
||||||
uid: true,
|
|
||||||
type: true,
|
|
||||||
externalCalendarId: true,
|
|
||||||
credentialId: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
payment: true,
|
|
||||||
paid: true,
|
|
||||||
eventType: {
|
|
||||||
select: {
|
|
||||||
recurringEvent: true,
|
|
||||||
title: true,
|
|
||||||
description: true,
|
|
||||||
requiresConfirmation: true,
|
|
||||||
price: true,
|
|
||||||
currency: true,
|
|
||||||
length: true,
|
|
||||||
workflows: {
|
|
||||||
include: {
|
|
||||||
workflow: {
|
|
||||||
include: {
|
|
||||||
steps: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
uid: true,
|
|
||||||
eventTypeId: true,
|
|
||||||
destinationCalendar: true,
|
|
||||||
smsReminderNumber: true,
|
|
||||||
workflowReminders: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
if (!bookingToDelete || !bookingToDelete.user) {
|
|
||||||
throw new HttpError({ statusCode: 404, message: "Booking not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if ((!session || session.user?.id !== bookingToDelete.user?.id) && bookingToDelete.startTime < new Date()) {
|
|
||||||
throw new HttpError({ statusCode: 403, message: "Cannot cancel past events" });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!bookingToDelete.userId) {
|
|
||||||
throw new HttpError({ statusCode: 404, message: "User not found" });
|
|
||||||
}
|
|
||||||
|
|
||||||
const organizer = await prisma.user.findFirstOrThrow({
|
|
||||||
where: {
|
|
||||||
id: bookingToDelete.userId,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
name: true,
|
|
||||||
email: true,
|
|
||||||
timeZone: true,
|
|
||||||
locale: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const attendeesListPromises = bookingToDelete.attendees.map(async (attendee) => {
|
|
||||||
return {
|
|
||||||
name: attendee.name,
|
|
||||||
email: attendee.email,
|
|
||||||
timeZone: attendee.timeZone,
|
|
||||||
language: {
|
|
||||||
translate: await getTranslation(attendee.locale ?? "en", "common"),
|
|
||||||
locale: attendee.locale ?? "en",
|
|
||||||
},
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const attendeesList = await Promise.all(attendeesListPromises);
|
|
||||||
const tOrganizer = await getTranslation(organizer.locale ?? "en", "common");
|
|
||||||
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
title: bookingToDelete?.title,
|
|
||||||
type: (bookingToDelete?.eventType?.title as string) || bookingToDelete?.title,
|
|
||||||
description: bookingToDelete?.description || "",
|
|
||||||
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
|
|
||||||
startTime: bookingToDelete?.startTime ? dayjs(bookingToDelete.startTime).format() : "",
|
|
||||||
endTime: bookingToDelete?.endTime ? dayjs(bookingToDelete.endTime).format() : "",
|
|
||||||
organizer: {
|
|
||||||
email: organizer.email,
|
|
||||||
name: organizer.name ?? "Nameless",
|
|
||||||
timeZone: organizer.timeZone,
|
|
||||||
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
|
|
||||||
},
|
|
||||||
attendees: attendeesList,
|
|
||||||
uid: bookingToDelete?.uid,
|
|
||||||
/* Include recurringEvent information only when cancelling all bookings */
|
|
||||||
recurringEvent: allRemainingBookings
|
|
||||||
? parseRecurringEvent(bookingToDelete.eventType?.recurringEvent)
|
|
||||||
: undefined,
|
|
||||||
location: bookingToDelete?.location,
|
|
||||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
|
||||||
cancellationReason: cancellationReason,
|
|
||||||
};
|
|
||||||
// Hook up the webhook logic here
|
|
||||||
const eventTrigger: WebhookTriggerEvents = "BOOKING_CANCELLED";
|
|
||||||
// Send Webhook call if hooked to BOOKING.CANCELLED
|
|
||||||
const subscriberOptions = {
|
|
||||||
userId: bookingToDelete.userId,
|
|
||||||
eventTypeId: (bookingToDelete.eventTypeId as number) || 0,
|
|
||||||
triggerEvent: eventTrigger,
|
|
||||||
};
|
|
||||||
|
|
||||||
const eventTypeInfo: EventTypeInfo = {
|
|
||||||
eventTitle: bookingToDelete?.eventType?.title || null,
|
|
||||||
eventDescription: bookingToDelete?.eventType?.description || null,
|
|
||||||
requiresConfirmation: bookingToDelete?.eventType?.requiresConfirmation || null,
|
|
||||||
price: bookingToDelete?.eventType?.price || null,
|
|
||||||
currency: bookingToDelete?.eventType?.currency || null,
|
|
||||||
length: bookingToDelete?.eventType?.length || null,
|
|
||||||
};
|
|
||||||
|
|
||||||
const webhooks = await getWebhooks(subscriberOptions);
|
|
||||||
const promises = webhooks.map((webhook) =>
|
|
||||||
sendPayload(webhook.secret, eventTrigger, new Date().toISOString(), webhook, {
|
|
||||||
...evt,
|
|
||||||
...eventTypeInfo,
|
|
||||||
status: "CANCELLED",
|
|
||||||
}).catch((e) => {
|
|
||||||
console.error(`Error executing webhook for event: ${eventTrigger}, URL: ${webhook.subscriberUrl}`, e);
|
|
||||||
})
|
|
||||||
);
|
|
||||||
await Promise.all(promises);
|
|
||||||
|
|
||||||
//Workflows - schedule reminders
|
|
||||||
if (bookingToDelete.eventType?.workflows) {
|
|
||||||
await sendCancelledReminders(
|
|
||||||
bookingToDelete.eventType?.workflows,
|
|
||||||
bookingToDelete.smsReminderNumber,
|
|
||||||
evt
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
let updatedBookings: {
|
|
||||||
uid: string;
|
|
||||||
workflowReminders: WorkflowReminder[];
|
|
||||||
scheduledJobs: string[];
|
|
||||||
}[] = [];
|
|
||||||
|
|
||||||
// by cancelling first, and blocking whilst doing so; we can ensure a cancel
|
|
||||||
// action always succeeds even if subsequent integrations fail cancellation.
|
|
||||||
if (bookingToDelete.eventType?.recurringEvent && bookingToDelete.recurringEventId && allRemainingBookings) {
|
|
||||||
const recurringEventId = bookingToDelete.recurringEventId;
|
|
||||||
// Proceed to mark as cancelled all remaining recurring events instances (greater than or equal to right now)
|
|
||||||
await prisma.booking.updateMany({
|
|
||||||
where: {
|
|
||||||
recurringEventId,
|
|
||||||
startTime: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.CANCELLED,
|
|
||||||
cancellationReason: cancellationReason,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const allUpdatedBookings = await prisma.booking.findMany({
|
|
||||||
where: {
|
|
||||||
recurringEventId: bookingToDelete.recurringEventId,
|
|
||||||
startTime: {
|
|
||||||
gte: new Date(),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
workflowReminders: true,
|
|
||||||
uid: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
updatedBookings = updatedBookings.concat(allUpdatedBookings);
|
|
||||||
} else {
|
|
||||||
const updatedBooking = await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
uid,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.CANCELLED,
|
|
||||||
cancellationReason: cancellationReason,
|
|
||||||
},
|
|
||||||
select: {
|
|
||||||
workflowReminders: true,
|
|
||||||
uid: true,
|
|
||||||
scheduledJobs: true,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
updatedBookings.push(updatedBooking);
|
|
||||||
}
|
|
||||||
|
|
||||||
/** TODO: Remove this without breaking functionality */
|
|
||||||
if (bookingToDelete.location === DailyLocationType) {
|
|
||||||
bookingToDelete.user.credentials.push(FAKE_DAILY_CREDENTIAL);
|
|
||||||
}
|
|
||||||
|
|
||||||
const apiDeletes = [];
|
|
||||||
|
|
||||||
const bookingCalendarReference = bookingToDelete.references.find((reference) =>
|
|
||||||
reference.type.includes("_calendar")
|
|
||||||
);
|
|
||||||
|
|
||||||
if (bookingCalendarReference) {
|
|
||||||
const { credentialId, uid, externalCalendarId } = bookingCalendarReference;
|
|
||||||
// If the booking calendar reference contains a credentialId
|
|
||||||
if (credentialId) {
|
|
||||||
// Find the correct calendar credential under user credentials
|
|
||||||
const calendarCredential = bookingToDelete.user.credentials.find(
|
|
||||||
(credential) => credential.id === credentialId
|
|
||||||
);
|
|
||||||
if (calendarCredential) {
|
|
||||||
const calendar = getCalendar(calendarCredential);
|
|
||||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId));
|
|
||||||
}
|
|
||||||
// For bookings made before the refactor we go through the old behaviour of running through each calendar credential
|
|
||||||
} else {
|
|
||||||
bookingToDelete.user.credentials
|
|
||||||
.filter((credential) => credential.type.endsWith("_calendar"))
|
|
||||||
.forEach((credential) => {
|
|
||||||
const calendar = getCalendar(credential);
|
|
||||||
apiDeletes.push(calendar?.deleteEvent(uid, evt, externalCalendarId));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const bookingVideoReference = bookingToDelete.references.find((reference) =>
|
|
||||||
reference.type.includes("_video")
|
|
||||||
);
|
|
||||||
|
|
||||||
// If the video reference has a credentialId find the specific credential
|
|
||||||
if (bookingVideoReference && bookingVideoReference.credentialId) {
|
|
||||||
const { credentialId, uid } = bookingVideoReference;
|
|
||||||
if (credentialId) {
|
|
||||||
const videoCredential = bookingToDelete.user.credentials.find(
|
|
||||||
(credential) => credential.id === credentialId
|
|
||||||
);
|
|
||||||
|
|
||||||
if (videoCredential) {
|
|
||||||
apiDeletes.push(deleteMeeting(videoCredential, uid));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// For bookings made before this refactor we go through the old behaviour of running through each video credential
|
|
||||||
} else {
|
|
||||||
bookingToDelete.user.credentials
|
|
||||||
.filter((credential) => credential.type.endsWith("_video"))
|
|
||||||
.forEach((credential) => {
|
|
||||||
apiDeletes.push(deleteMeeting(credential, uid));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// Avoiding taking care of recurrence for now as Payments are not supported with Recurring Events at the moment
|
|
||||||
if (bookingToDelete && bookingToDelete.paid) {
|
|
||||||
const evt: CalendarEvent = {
|
|
||||||
type: bookingToDelete?.eventType?.title as string,
|
|
||||||
title: bookingToDelete.title,
|
|
||||||
description: bookingToDelete.description ?? "",
|
|
||||||
customInputs: isPrismaObjOrUndefined(bookingToDelete.customInputs),
|
|
||||||
startTime: bookingToDelete.startTime.toISOString(),
|
|
||||||
endTime: bookingToDelete.endTime.toISOString(),
|
|
||||||
organizer: {
|
|
||||||
email: bookingToDelete.user?.email ?? "dev@calendso.com",
|
|
||||||
name: bookingToDelete.user?.name ?? "no user",
|
|
||||||
timeZone: bookingToDelete.user?.timeZone ?? "",
|
|
||||||
language: { translate: tOrganizer, locale: organizer.locale ?? "en" },
|
|
||||||
},
|
|
||||||
attendees: attendeesList,
|
|
||||||
location: bookingToDelete.location ?? "",
|
|
||||||
uid: bookingToDelete.uid ?? "",
|
|
||||||
destinationCalendar: bookingToDelete?.destinationCalendar || bookingToDelete?.user.destinationCalendar,
|
|
||||||
};
|
|
||||||
await refund(bookingToDelete, evt);
|
|
||||||
await prisma.booking.update({
|
|
||||||
where: {
|
|
||||||
id: bookingToDelete.id,
|
|
||||||
},
|
|
||||||
data: {
|
|
||||||
status: BookingStatus.REJECTED,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// We skip the deletion of the event, because that would also delete the payment reference, which we should keep
|
|
||||||
await apiDeletes;
|
|
||||||
return res.status(200).json({ message: "Booking successfully deleted." });
|
|
||||||
}
|
|
||||||
|
|
||||||
const attendeeDeletes = prisma.attendee.deleteMany({
|
|
||||||
where: {
|
|
||||||
bookingId: bookingToDelete.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bookingReferenceDeletes = prisma.bookingReference.deleteMany({
|
|
||||||
where: {
|
|
||||||
bookingId: bookingToDelete.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// delete scheduled jobs of cancelled bookings
|
|
||||||
updatedBookings.forEach((booking) => {
|
|
||||||
cancelScheduledJobs(booking);
|
|
||||||
});
|
|
||||||
|
|
||||||
//Workflows - delete all reminders for bookings
|
|
||||||
const remindersToDelete: PrismaPromise<Prisma.BatchPayload>[] = [];
|
|
||||||
updatedBookings.forEach((booking) => {
|
|
||||||
booking.workflowReminders.forEach((reminder) => {
|
|
||||||
if (reminder.scheduled && reminder.referenceId) {
|
|
||||||
if (reminder.method === WorkflowMethods.EMAIL) {
|
|
||||||
deleteScheduledEmailReminder(reminder.referenceId);
|
|
||||||
} else if (reminder.method === WorkflowMethods.SMS) {
|
|
||||||
deleteScheduledSMSReminder(reminder.referenceId);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const reminderToDelete = prisma.workflowReminder.deleteMany({
|
|
||||||
where: {
|
|
||||||
id: reminder.id,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
remindersToDelete.push(reminderToDelete);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
await Promise.all([apiDeletes, attendeeDeletes, bookingReferenceDeletes].concat(remindersToDelete));
|
|
||||||
|
|
||||||
await sendCancelledEmails(evt);
|
|
||||||
|
|
||||||
res.status(204).end();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default defaultHandler({
|
export default defaultHandler({
|
||||||
|
|||||||
@@ -32,6 +32,18 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
timeZone: "America/Chihuahua",
|
timeZone: "America/Chihuahua",
|
||||||
language,
|
language,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
email: "pro@example.com",
|
||||||
|
name: "pro@example.com",
|
||||||
|
timeZone: "America/Chihuahua",
|
||||||
|
language,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
email: "pro@example.com",
|
||||||
|
name: "pro@example.com",
|
||||||
|
timeZone: "America/Chihuahua",
|
||||||
|
language,
|
||||||
|
},
|
||||||
],
|
],
|
||||||
location: "Zoom video",
|
location: "Zoom video",
|
||||||
destinationCalendar: null,
|
destinationCalendar: null,
|
||||||
@@ -48,7 +60,7 @@ const handler = async (req: NextApiRequest, res: NextApiResponse) => {
|
|||||||
res.setHeader("Content-Type", "text/html");
|
res.setHeader("Content-Type", "text/html");
|
||||||
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
res.setHeader("Cache-Control", "no-cache, no-store, private, must-revalidate");
|
||||||
res.write(
|
res.write(
|
||||||
renderEmail("OrganizerRequestReminderEmail", {
|
renderEmail("AttendeeScheduledEmail", {
|
||||||
attendee: evt.attendees[0],
|
attendee: evt.attendees[0],
|
||||||
calEvent: evt,
|
calEvent: evt,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
import { ImageResponse } from "@vercel/og";
|
||||||
|
import { NextApiRequest } from "next";
|
||||||
|
import type { SatoriOptions } from "satori";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { Meeting, App, Generic } from "@calcom/lib/OgImages";
|
||||||
|
|
||||||
|
const calFont = fetch(new URL("../../../../public/fonts/cal.ttf", import.meta.url)).then((res) =>
|
||||||
|
res.arrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
const interFont = fetch(new URL("../../../../public/fonts/Inter-Regular.ttf", import.meta.url)).then((res) =>
|
||||||
|
res.arrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
const interFontMedium = fetch(new URL("../../../../public/fonts/Inter-Medium.ttf", import.meta.url)).then(
|
||||||
|
(res) => res.arrayBuffer()
|
||||||
|
);
|
||||||
|
|
||||||
|
export const config = {
|
||||||
|
runtime: "experimental-edge",
|
||||||
|
};
|
||||||
|
|
||||||
|
const meetingSchema = z.object({
|
||||||
|
imageType: z.literal("meeting"),
|
||||||
|
title: z.string(),
|
||||||
|
names: z.string().array(),
|
||||||
|
usernames: z.string().array(),
|
||||||
|
meetingProfileName: z.string(),
|
||||||
|
meetingImage: z.string().nullable().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const appSchema = z.object({
|
||||||
|
imageType: z.literal("app"),
|
||||||
|
name: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
slug: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const genericSchema = z.object({
|
||||||
|
imageType: z.literal("generic"),
|
||||||
|
title: z.string(),
|
||||||
|
description: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
export default async function handler(req: NextApiRequest) {
|
||||||
|
const { searchParams } = new URL(`${req.url}`);
|
||||||
|
const imageType = searchParams.get("type");
|
||||||
|
|
||||||
|
const [calFontData, interFontData, interFontMediumData] = await Promise.all([
|
||||||
|
calFont,
|
||||||
|
interFont,
|
||||||
|
interFontMedium,
|
||||||
|
]);
|
||||||
|
const ogConfig = {
|
||||||
|
fonts: [
|
||||||
|
{ name: "inter", data: interFontData, weight: 400 },
|
||||||
|
{ name: "inter", data: interFontMediumData, weight: 500 },
|
||||||
|
{ name: "cal", data: calFontData, weight: 400 },
|
||||||
|
{ name: "cal", data: calFontData, weight: 600 },
|
||||||
|
] as SatoriOptions["fonts"],
|
||||||
|
};
|
||||||
|
|
||||||
|
switch (imageType) {
|
||||||
|
case "meeting": {
|
||||||
|
const { names, usernames, title, meetingProfileName, meetingImage } = meetingSchema.parse({
|
||||||
|
names: searchParams.getAll("names"),
|
||||||
|
usernames: searchParams.getAll("usernames"),
|
||||||
|
title: searchParams.get("title"),
|
||||||
|
meetingProfileName: searchParams.get("meetingProfileName"),
|
||||||
|
meetingImage: searchParams.get("meetingImage"),
|
||||||
|
imageType,
|
||||||
|
});
|
||||||
|
return new ImageResponse(
|
||||||
|
(
|
||||||
|
<Meeting
|
||||||
|
title={title}
|
||||||
|
profile={{ name: meetingProfileName, image: meetingImage }}
|
||||||
|
users={names.map((name, index) => ({ name, username: usernames[index] }))}
|
||||||
|
/>
|
||||||
|
),
|
||||||
|
ogConfig
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case "app": {
|
||||||
|
const { name, description, slug } = appSchema.parse({
|
||||||
|
name: searchParams.get("name"),
|
||||||
|
description: searchParams.get("description"),
|
||||||
|
slug: searchParams.get("slug"),
|
||||||
|
imageType,
|
||||||
|
});
|
||||||
|
return new ImageResponse(<App name={name} description={description} slug={slug} />, ogConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
case "generic": {
|
||||||
|
const { title, description } = genericSchema.parse({
|
||||||
|
title: searchParams.get("title"),
|
||||||
|
description: searchParams.get("description"),
|
||||||
|
imageType,
|
||||||
|
});
|
||||||
|
|
||||||
|
return new ImageResponse(<Generic title={title} description={description} />, ogConfig);
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
return new Response("What you're looking for is not here..", { status: 404 });
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
import crypto from "crypto";
|
import crypto from "crypto";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
|
||||||
|
import { CAL_URL, WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
import { getPlaceholderAvatar } from "@calcom/lib/getPlaceholderAvatar";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
@@ -11,6 +12,7 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
const username = req.query.username as string;
|
const username = req.query.username as string;
|
||||||
const teamname = req.query.teamname as string;
|
const teamname = req.query.teamname as string;
|
||||||
let identity;
|
let identity;
|
||||||
|
let linksToThisRoute = false;
|
||||||
if (username) {
|
if (username) {
|
||||||
const user = await prisma.user.findUnique({
|
const user = await prisma.user.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -26,6 +28,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
email: user?.email,
|
email: user?.email,
|
||||||
avatar: user?.avatar,
|
avatar: user?.avatar,
|
||||||
};
|
};
|
||||||
|
linksToThisRoute =
|
||||||
|
identity.avatar === `${CAL_URL}/${username}/avatar.png` ||
|
||||||
|
identity.avatar === `${WEBAPP_URL}/${username}/avatar.png`;
|
||||||
} else if (teamname) {
|
} else if (teamname) {
|
||||||
const team = await prisma.team.findUnique({
|
const team = await prisma.team.findUnique({
|
||||||
where: {
|
where: {
|
||||||
@@ -40,6 +45,9 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
shouldDefaultBeNameBased: true,
|
shouldDefaultBeNameBased: true,
|
||||||
avatar: team?.logo,
|
avatar: team?.logo,
|
||||||
};
|
};
|
||||||
|
linksToThisRoute =
|
||||||
|
identity.avatar === `${CAL_URL}/team/${teamname}/avatar.png` ||
|
||||||
|
identity.avatar === `${WEBAPP_URL}/team/${teamname}/avatar.png`;
|
||||||
}
|
}
|
||||||
|
|
||||||
const emailMd5 = crypto
|
const emailMd5 = crypto
|
||||||
@@ -47,7 +55,8 @@ export default async function handler(req: NextApiRequest, res: NextApiResponse)
|
|||||||
.update((identity?.email as string) || "guest@example.com")
|
.update((identity?.email as string) || "guest@example.com")
|
||||||
.digest("hex");
|
.digest("hex");
|
||||||
const img = identity?.avatar;
|
const img = identity?.avatar;
|
||||||
if (!img) {
|
// If image isn't set or links to this route itself, use default avatar
|
||||||
|
if (!img || linksToThisRoute) {
|
||||||
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
let defaultSrc = defaultAvatarSrc({ md5: emailMd5 });
|
||||||
if (identity?.shouldDefaultBeNameBased) {
|
if (identity?.shouldDefaultBeNameBased) {
|
||||||
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
defaultSrc = getPlaceholderAvatar(null, identity.name);
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ import prisma from "@calcom/prisma";
|
|||||||
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import App from "@components/v2/apps/App";
|
import App from "@components/apps/App";
|
||||||
|
|
||||||
const components = {
|
const components = {
|
||||||
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
|
a: ({ href = "", ...otherProps }: JSX.IntrinsicElements["a"]) => (
|
||||||
@@ -34,6 +34,7 @@ function SingleAppPage({ data, source }: inferSSRProps<typeof getStaticProps>) {
|
|||||||
return (
|
return (
|
||||||
<App
|
<App
|
||||||
name={data.name}
|
name={data.name}
|
||||||
|
description={data.description}
|
||||||
isGlobal={data.isGlobal}
|
isGlobal={data.isGlobal}
|
||||||
slug={data.slug}
|
slug={data.slug}
|
||||||
variant={data.variant}
|
variant={data.variant}
|
||||||
|
|||||||
@@ -2,8 +2,8 @@ import { GetStaticPaths, InferGetStaticPropsType } from "next";
|
|||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
|
|
||||||
|
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
|
||||||
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
|
import { getStaticProps } from "@calcom/app-store/_pages/setup/_getStaticProps";
|
||||||
import { AppSetupPage } from "@calcom/app-store/_pages/v2/setup";
|
|
||||||
|
|
||||||
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -23,7 +23,7 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
return <AppSetupPage slug={`${slug}-V2`} {...props} />;
|
return <AppSetupPage slug={slug} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import AppStoreCategories from "@calcom/ui/v2/core/apps/Categories";
|
|||||||
import TrendingAppsSlider from "@calcom/ui/v2/core/apps/TrendingAppsSlider";
|
import TrendingAppsSlider from "@calcom/ui/v2/core/apps/TrendingAppsSlider";
|
||||||
import AppsLayout from "@calcom/ui/v2/core/layouts/AppsLayout";
|
import AppsLayout from "@calcom/ui/v2/core/layouts/AppsLayout";
|
||||||
|
|
||||||
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
export default function Apps({ appStore, categories }: InferGetStaticPropsType<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -22,6 +24,8 @@ export default function Apps({ appStore, categories }: InferGetStaticPropsType<t
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const getServerSideProps = async (context: NextPageContext) => {
|
export const getServerSideProps = async (context: NextPageContext) => {
|
||||||
|
const ssg = await ssgInit(context);
|
||||||
|
|
||||||
const session = await getSession(context);
|
const session = await getSession(context);
|
||||||
|
|
||||||
let appStore;
|
let appStore;
|
||||||
@@ -42,6 +46,7 @@ export const getServerSideProps = async (context: NextPageContext) => {
|
|||||||
}, {} as Record<string, number>);
|
}, {} as Record<string, number>);
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
trpcState: ssg.dehydrate(),
|
||||||
categories: Object.entries(categories)
|
categories: Object.entries(categories)
|
||||||
.map(([name, count]): { name: AppCategories; count: number } => ({
|
.map(([name, count]): { name: AppCategories; count: number } => ({
|
||||||
name: name as AppCategories,
|
name: name as AppCategories,
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { InferGetServerSidePropsType } from "next";
|
import { useRouter } from "next/router";
|
||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
import { AppSettings } from "@calcom/app-store/_components/AppSettings";
|
||||||
@@ -9,7 +9,6 @@ import { inferQueryOutput, trpc } from "@calcom/trpc/react";
|
|||||||
import { App } from "@calcom/types/App";
|
import { App } from "@calcom/types/App";
|
||||||
import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
|
import { AppGetServerSidePropsContext } from "@calcom/types/AppGetServerSideProps";
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import SkeletonLoader from "@calcom/ui/apps/SkeletonLoader";
|
|
||||||
import { Alert } from "@calcom/ui/v2/core/Alert";
|
import { Alert } from "@calcom/ui/v2/core/Alert";
|
||||||
import Button from "@calcom/ui/v2/core/Button";
|
import Button from "@calcom/ui/v2/core/Button";
|
||||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||||
@@ -20,18 +19,21 @@ import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/Disconnect
|
|||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import { CalendarListContainer } from "@components/v2/apps/CalendarListContainer";
|
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||||
import IntegrationListItem from "@components/v2/apps/IntegrationListItem";
|
import IntegrationListItem from "@components/apps/IntegrationListItem";
|
||||||
|
import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
|
||||||
|
|
||||||
function ConnectOrDisconnectIntegrationButton(props: {
|
function ConnectOrDisconnectIntegrationButton(props: {
|
||||||
credentialIds: number[];
|
credentialIds: number[];
|
||||||
type: App["type"];
|
type: App["type"];
|
||||||
isGlobal?: boolean;
|
isGlobal?: boolean;
|
||||||
installed?: boolean;
|
installed?: boolean;
|
||||||
|
invalidCredentialIds?: number[];
|
||||||
}) {
|
}) {
|
||||||
|
const { type, credentialIds, isGlobal, installed, invalidCredentialIds } = props;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [credentialId] = props.credentialIds;
|
const [credentialId] = credentialIds;
|
||||||
const type = props.type;
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const handleOpenChange = () => {
|
const handleOpenChange = () => {
|
||||||
utils.invalidateQueries(["viewer.integrations"]);
|
utils.invalidateQueries(["viewer.integrations"]);
|
||||||
@@ -48,6 +50,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<DisconnectIntegration
|
<DisconnectIntegration
|
||||||
credentialId={credentialId}
|
credentialId={credentialId}
|
||||||
@@ -57,7 +60,8 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
if (!props.installed) {
|
|
||||||
|
if (!installed) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center truncate">
|
<div className="flex items-center truncate">
|
||||||
<Alert severity="warning" title={t("not_installed")} />
|
<Alert severity="warning" title={t("not_installed")} />
|
||||||
@@ -65,7 +69,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
/** We don't need to "Connect", just show that it's installed */
|
/** We don't need to "Connect", just show that it's installed */
|
||||||
if (props.isGlobal) {
|
if (isGlobal) {
|
||||||
return (
|
return (
|
||||||
<div className="truncate px-3 py-2">
|
<div className="truncate px-3 py-2">
|
||||||
<h3 className="text-sm font-medium text-gray-700">{t("default")}</h3>
|
<h3 className="text-sm font-medium text-gray-700">{t("default")}</h3>
|
||||||
@@ -74,7 +78,7 @@ function ConnectOrDisconnectIntegrationButton(props: {
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<InstallAppButton
|
<InstallAppButton
|
||||||
type={props.type}
|
type={type}
|
||||||
render={(buttonProps) => (
|
render={(buttonProps) => (
|
||||||
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
|
<Button color="secondary" {...buttonProps} data-testid="integration-connection-button">
|
||||||
{t("install")}
|
{t("install")}
|
||||||
@@ -98,28 +102,32 @@ interface IntegrationsListProps {
|
|||||||
const IntegrationsList = ({ data }: IntegrationsListProps) => {
|
const IntegrationsList = ({ data }: IntegrationsListProps) => {
|
||||||
return (
|
return (
|
||||||
<List className="flex flex-col gap-6" noBorderTreatment>
|
<List className="flex flex-col gap-6" noBorderTreatment>
|
||||||
{data.items.map((item) => (
|
{data.items
|
||||||
<IntegrationListItem
|
.filter((item) => item.invalidCredentialIds)
|
||||||
name={item.name}
|
.map((item) => (
|
||||||
slug={item.slug}
|
<IntegrationListItem
|
||||||
key={item.title}
|
name={item.name}
|
||||||
title={item.title}
|
slug={item.slug}
|
||||||
logo={item.logo}
|
key={item.title}
|
||||||
description={item.description}
|
title={item.title}
|
||||||
separate={true}
|
logo={item.logo}
|
||||||
actions={
|
description={item.description}
|
||||||
<div className="flex w-16 justify-end">
|
separate={true}
|
||||||
<ConnectOrDisconnectIntegrationButton
|
invalidCredential={item.invalidCredentialIds.length > 0}
|
||||||
credentialIds={item.credentialIds}
|
actions={
|
||||||
type={item.type}
|
<div className="flex w-16 justify-end">
|
||||||
isGlobal={item.isGlobal}
|
<ConnectOrDisconnectIntegrationButton
|
||||||
installed
|
credentialIds={item.credentialIds}
|
||||||
/>
|
type={item.type}
|
||||||
</div>
|
isGlobal={item.isGlobal}
|
||||||
}>
|
installed
|
||||||
<AppSettings slug={item.slug} />
|
invalidCredentialIds={item.invalidCredentialIds}
|
||||||
</IntegrationListItem>
|
/>
|
||||||
))}
|
</div>
|
||||||
|
}>
|
||||||
|
<AppSettings slug={item.slug} />
|
||||||
|
</IntegrationListItem>
|
||||||
|
))}
|
||||||
</List>
|
</List>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -188,9 +196,10 @@ const querySchema = z.object({
|
|||||||
category: z.nativeEnum(InstalledAppVariants),
|
category: z.nativeEnum(InstalledAppVariants),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function InstalledApps({ category }: InferGetServerSidePropsType<typeof getServerSideProps>) {
|
export default function InstalledApps() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
const router = useRouter();
|
||||||
|
const category = router.query.category;
|
||||||
return (
|
return (
|
||||||
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
<InstalledAppsLayout heading={t("installed_apps")} subtitle={t("manage_your_connected_apps")}>
|
||||||
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && (
|
{(category === InstalledAppVariants.payment || category === InstalledAppVariants.conferencing) && (
|
||||||
|
|||||||
@@ -4,21 +4,21 @@ import { getCsrfToken, signIn } from "next-auth/react";
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm, FormProvider } from "react-hook-form";
|
||||||
import { FaGoogle } from "react-icons/fa";
|
import { FaGoogle } from "react-icons/fa";
|
||||||
|
|
||||||
|
import { isSAMLLoginEnabled, samlProductID, samlTenantID } from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
import { getSafeRedirectUrl } from "@calcom/lib/getSafeRedirectUrl";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import { Alert } from "@calcom/ui/Alert";
|
import { Alert } from "@calcom/ui/Alert";
|
||||||
import { Button, EmailField, Form, PasswordField } from "@calcom/ui/v2";
|
import { Button, EmailField, PasswordField } from "@calcom/ui/v2";
|
||||||
import SAMLLogin from "@calcom/ui/v2/modules/auth/SAMLLogin";
|
import SAMLLogin from "@calcom/ui/v2/modules/auth/SAMLLogin";
|
||||||
|
|
||||||
import { ErrorCode, getSession } from "@lib/auth";
|
import { ErrorCode, getSession } from "@lib/auth";
|
||||||
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
import { WEBAPP_URL, WEBSITE_URL } from "@lib/config/constants";
|
||||||
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID } from "@lib/saml";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import AddToHomescreen from "@components/AddToHomescreen";
|
import AddToHomescreen from "@components/AddToHomescreen";
|
||||||
@@ -39,15 +39,14 @@ export default function Login({
|
|||||||
csrfToken,
|
csrfToken,
|
||||||
isGoogleLoginEnabled,
|
isGoogleLoginEnabled,
|
||||||
isSAMLLoginEnabled,
|
isSAMLLoginEnabled,
|
||||||
hostedCal,
|
|
||||||
samlTenantID,
|
samlTenantID,
|
||||||
samlProductID,
|
samlProductID,
|
||||||
}: inferSSRProps<typeof getServerSideProps>) {
|
}: inferSSRProps<typeof getServerSideProps>) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<LoginValues>();
|
const methods = useForm<LoginValues>();
|
||||||
const { formState } = form;
|
|
||||||
const { isSubmitting } = formState;
|
const { register, formState } = methods;
|
||||||
|
|
||||||
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
const [twoFactorRequired, setTwoFactorRequired] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
const [errorMessage, setErrorMessage] = useState<string | null>(null);
|
||||||
@@ -86,7 +85,7 @@ export default function Login({
|
|||||||
<Button
|
<Button
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setTwoFactorRequired(false);
|
setTwoFactorRequired(false);
|
||||||
form.setValue("totpCode", "");
|
methods.setValue("totpCode", "");
|
||||||
}}
|
}}
|
||||||
StartIcon={Icon.FiArrowLeft}
|
StartIcon={Icon.FiArrowLeft}
|
||||||
color="minimal">
|
color="minimal">
|
||||||
@@ -94,6 +93,23 @@ export default function Login({
|
|||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const onSubmit = async (values: LoginValues) => {
|
||||||
|
setErrorMessage(null);
|
||||||
|
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
||||||
|
const res = await signIn<"credentials">("credentials", {
|
||||||
|
...values,
|
||||||
|
callbackUrl,
|
||||||
|
redirect: false,
|
||||||
|
});
|
||||||
|
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
|
||||||
|
// we're logged in! let's do a hard refresh to the desired url
|
||||||
|
else if (!res.error) router.push(callbackUrl);
|
||||||
|
// reveal two factor input if required
|
||||||
|
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
|
||||||
|
// fallback if error not found
|
||||||
|
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<AuthContainer
|
<AuthContainer
|
||||||
@@ -102,100 +118,81 @@ export default function Login({
|
|||||||
showLogo
|
showLogo
|
||||||
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
heading={twoFactorRequired ? t("2fa_code") : t("welcome_back")}
|
||||||
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
|
footerText={twoFactorRequired ? TwoFactorFooter : LoginFooter}>
|
||||||
<Form
|
<FormProvider {...methods}>
|
||||||
form={form}
|
<form onSubmit={methods.handleSubmit(onSubmit)} data-testid="login-form">
|
||||||
handleSubmit={async (values) => {
|
<div>
|
||||||
setErrorMessage(null);
|
<input defaultValue={csrfToken || undefined} type="hidden" hidden {...register("csrfToken")} />
|
||||||
telemetry.event(telemetryEventTypes.login, collectPageParameters());
|
</div>
|
||||||
const res = await signIn<"credentials">("credentials", {
|
<div className="space-y-6">
|
||||||
...values,
|
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
||||||
callbackUrl,
|
<EmailField
|
||||||
redirect: false,
|
id="email"
|
||||||
});
|
label={t("email_address")}
|
||||||
if (!res) setErrorMessage(errorMessages[ErrorCode.InternalServerError]);
|
defaultValue={router.query.email as string}
|
||||||
// we're logged in! let's do a hard refresh to the desired url
|
placeholder="john.doe@example.com"
|
||||||
else if (!res.error) router.push(callbackUrl);
|
|
||||||
// reveal two factor input if required
|
|
||||||
else if (res.error === ErrorCode.SecondFactorRequired) setTwoFactorRequired(true);
|
|
||||||
// fallback if error not found
|
|
||||||
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
|
||||||
}}
|
|
||||||
data-testid="login-form">
|
|
||||||
<div>
|
|
||||||
<input
|
|
||||||
defaultValue={csrfToken || undefined}
|
|
||||||
type="hidden"
|
|
||||||
hidden
|
|
||||||
{...form.register("csrfToken")}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="space-y-6">
|
|
||||||
<div className={classNames("space-y-6", { hidden: twoFactorRequired })}>
|
|
||||||
<EmailField
|
|
||||||
id="email"
|
|
||||||
label={t("email_address")}
|
|
||||||
defaultValue={router.query.email as string}
|
|
||||||
placeholder="john.doe@example.com"
|
|
||||||
required
|
|
||||||
{...form.register("email")}
|
|
||||||
/>
|
|
||||||
<div className="relative">
|
|
||||||
<div className="absolute right-0 -top-[6px] z-10">
|
|
||||||
<Link href="/auth/forgot-password">
|
|
||||||
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
|
||||||
{t("forgot")}
|
|
||||||
</a>
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
<PasswordField
|
|
||||||
id="password"
|
|
||||||
autoComplete="current-password"
|
|
||||||
required
|
required
|
||||||
className="mb-0"
|
{...register("email")}
|
||||||
{...form.register("password")}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="relative">
|
||||||
|
<div className="absolute right-0 -top-[6px] z-10">
|
||||||
|
<Link href="/auth/forgot-password">
|
||||||
|
<a tabIndex={-1} className="text-sm font-medium text-gray-600">
|
||||||
|
{t("forgot")}
|
||||||
|
</a>
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<PasswordField
|
||||||
|
id="password"
|
||||||
|
autoComplete="current-password"
|
||||||
|
required
|
||||||
|
className="mb-0"
|
||||||
|
{...register("password")}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
|
||||||
|
|
||||||
{twoFactorRequired && <TwoFactor center />}
|
{twoFactorRequired && <TwoFactor center />}
|
||||||
|
|
||||||
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
{errorMessage && <Alert severity="error" title={errorMessage} />}
|
||||||
<Button type="submit" color="primary" disabled={isSubmitting} className="w-full justify-center">
|
<Button
|
||||||
{twoFactorRequired ? t("submit") : t("sign_in")}
|
type="submit"
|
||||||
</Button>
|
color="primary"
|
||||||
</div>
|
disabled={formState.isSubmitting}
|
||||||
</Form>
|
className="w-full justify-center">
|
||||||
{!twoFactorRequired && (
|
{twoFactorRequired ? t("submit") : t("sign_in")}
|
||||||
<>
|
</Button>
|
||||||
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
|
|
||||||
<div className="space-y-3">
|
|
||||||
{isGoogleLoginEnabled && (
|
|
||||||
<Button
|
|
||||||
color="secondary"
|
|
||||||
className="w-full justify-center"
|
|
||||||
data-testid="google"
|
|
||||||
StartIcon={FaGoogle}
|
|
||||||
onClick={async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
// track Google logins. Without personal data/payload
|
|
||||||
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
|
||||||
await signIn("google");
|
|
||||||
}}>
|
|
||||||
{t("signin_with_google")}
|
|
||||||
</Button>
|
|
||||||
)}
|
|
||||||
{isSAMLLoginEnabled && (
|
|
||||||
<SAMLLogin
|
|
||||||
email={form.getValues("email")}
|
|
||||||
samlTenantID={samlTenantID}
|
|
||||||
samlProductID={samlProductID}
|
|
||||||
hostedCal={hostedCal}
|
|
||||||
setErrorMessage={setErrorMessage}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
</form>
|
||||||
)}
|
{!twoFactorRequired && (
|
||||||
|
<>
|
||||||
|
{(isGoogleLoginEnabled || isSAMLLoginEnabled) && <hr className="my-8" />}
|
||||||
|
<div className="space-y-3">
|
||||||
|
{isGoogleLoginEnabled && (
|
||||||
|
<Button
|
||||||
|
color="secondary"
|
||||||
|
className="w-full justify-center"
|
||||||
|
data-testid="google"
|
||||||
|
StartIcon={FaGoogle}
|
||||||
|
onClick={async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
// track Google logins. Without personal data/payload
|
||||||
|
telemetry.event(telemetryEventTypes.googleLogin, collectPageParameters());
|
||||||
|
await signIn("google");
|
||||||
|
}}>
|
||||||
|
{t("signin_with_google")}
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
{isSAMLLoginEnabled && (
|
||||||
|
<SAMLLogin
|
||||||
|
samlTenantID={samlTenantID}
|
||||||
|
samlProductID={samlProductID}
|
||||||
|
setErrorMessage={setErrorMessage}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</FormProvider>
|
||||||
</AuthContainer>
|
</AuthContainer>
|
||||||
<AddToHomescreen />
|
<AddToHomescreen />
|
||||||
</>
|
</>
|
||||||
@@ -233,7 +230,6 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
trpcState: ssr.dehydrate(),
|
trpcState: ssr.dehydrate(),
|
||||||
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
isGoogleLoginEnabled: IS_GOOGLE_LOGIN_ENABLED,
|
||||||
isSAMLLoginEnabled,
|
isSAMLLoginEnabled,
|
||||||
hostedCal,
|
|
||||||
samlTenantID,
|
samlTenantID,
|
||||||
samlProductID,
|
samlProductID,
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -5,12 +5,18 @@ import { useEffect } from "react";
|
|||||||
|
|
||||||
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
|
import { getPremiumPlanPrice } from "@calcom/app-store/stripepayment/lib/utils";
|
||||||
import stripe from "@calcom/features/ee/payments/server/stripe";
|
import stripe from "@calcom/features/ee/payments/server/stripe";
|
||||||
|
import {
|
||||||
|
hostedCal,
|
||||||
|
isSAMLLoginEnabled,
|
||||||
|
samlProductID,
|
||||||
|
samlTenantID,
|
||||||
|
samlTenantProduct,
|
||||||
|
} from "@calcom/features/ee/sso/lib/saml";
|
||||||
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
import { checkUsername } from "@calcom/lib/server/checkUsername";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
|
|
||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { hostedCal, isSAMLLoginEnabled, samlProductID, samlTenantID, samlTenantProduct } from "@lib/saml";
|
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import { useEffect, useState, useRef } from "react";
|
|||||||
import z from "zod";
|
import z from "zod";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button } from "@calcom/ui/v2/";
|
import { Button } from "@calcom/ui/v2/";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
|
|
||||||
|
|||||||
@@ -162,7 +162,7 @@ export default function Availability({ schedule }: { schedule: number }) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
<div className="min-w-40 col-span-3 space-y-2 lg:col-span-1">
|
||||||
<div className="xl:max-w-80 w-full pr-4 sm:p-0">
|
<div className="xl:max-w-80 mt-4 w-full pr-4 sm:p-0">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
<label htmlFor="timeZone" className="block text-sm font-medium text-gray-700">
|
||||||
{t("timezone")}
|
{t("timezone")}
|
||||||
|
|||||||
@@ -1,5 +1,4 @@
|
|||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { useRef, useEffect } from "react";
|
|
||||||
|
|
||||||
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
import { NewScheduleButton, ScheduleListItem } from "@calcom/features/schedules";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
@@ -16,7 +15,6 @@ import SkeletonLoader from "@components/v2/availability/SkeletonLoader";
|
|||||||
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
|
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const animationParentRef = useRef(null);
|
|
||||||
|
|
||||||
const meQuery = trpc.useQuery(["viewer.me"]);
|
const meQuery = trpc.useQuery(["viewer.me"]);
|
||||||
|
|
||||||
@@ -50,9 +48,8 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Adds smooth delete button - item fades and old item slides into place
|
// Adds smooth delete button - item fades and old item slides into place
|
||||||
useEffect(() => {
|
|
||||||
animationParentRef.current && autoAnimate(animationParentRef.current);
|
const [animationParentRef] = useAutoAnimate<HTMLUListElement>();
|
||||||
}, [animationParentRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { GetStaticPaths, GetStaticProps } from "next";
|
import { GetStaticPaths, GetStaticProps } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Fragment, useEffect, useRef } from "react";
|
import { Fragment } from "react";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
import { WipeMyCalActionButton } from "@calcom/app-store/wipemycalother/components";
|
||||||
@@ -46,7 +46,7 @@ export default function Bookings() {
|
|||||||
});
|
});
|
||||||
|
|
||||||
// Animate page (tab) tranistions to look smoothing
|
// Animate page (tab) tranistions to look smoothing
|
||||||
const animationParentRef = useRef(null);
|
|
||||||
const buttonInView = useInViewObserver(() => {
|
const buttonInView = useInViewObserver(() => {
|
||||||
if (!query.isFetching && query.hasNextPage && query.status === "success") {
|
if (!query.isFetching && query.hasNextPage && query.status === "success") {
|
||||||
query.fetchNextPage();
|
query.fetchNextPage();
|
||||||
@@ -73,9 +73,7 @@ export default function Bookings() {
|
|||||||
return true;
|
return true;
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
animationParentRef.current && autoAnimate(animationParentRef.current);
|
|
||||||
}, [animationParentRef]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
|
<BookingLayout heading={t("bookings")} subtitle={t("bookings_description")}>
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calco
|
|||||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
import { Button } from "@calcom/ui/Button";
|
|
||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
|
import { Button } from "@calcom/ui/v2/core/Button";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
@@ -51,7 +51,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
<CustomBranding lightVal={props.profile?.brandColor} darkVal={props.profile?.darkBrandColor} />
|
<CustomBranding lightVal={props.profile?.brandColor} darkVal={props.profile?.darkBrandColor} />
|
||||||
<main className="h-full sm:flex sm:items-center">
|
<main className="h-full sm:flex sm:items-center">
|
||||||
<div className="mx-auto flex justify-center px-4 pt-4 pb-20 sm:block sm:p-0">
|
<div className="mx-auto flex justify-center px-4 pt-4 pb-20 sm:block sm:p-0">
|
||||||
<div className="inline-block transform overflow-hidden rounded-md border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle">
|
<div className="inline-block transform overflow-hidden rounded-md border bg-white px-8 pt-5 pb-4 text-left align-bottom transition-all dark:border-neutral-700 dark:bg-gray-800 sm:my-8 sm:w-full sm:max-w-lg sm:py-6 sm:align-middle">
|
||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
{error && (
|
{error && (
|
||||||
@@ -178,7 +178,7 @@ export default function Type(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
uid: uid,
|
uid: uid,
|
||||||
reason: cancellationReason,
|
cancellationReason: cancellationReason,
|
||||||
allRemainingBookings: !!props.recurringInstances,
|
allRemainingBookings: !!props.recurringInstances,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|||||||
import { getWorkingHours } from "@lib/availability";
|
import { getWorkingHours } from "@lib/availability";
|
||||||
import { GetBookingType } from "@lib/getBooking";
|
import { GetBookingType } from "@lib/getBooking";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
import { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type DynamicAvailabilityPageProps = inferSSRProps<typeof getServerSideProps> & EmbedProps;
|
||||||
|
|
||||||
export default function Type(props: DynamicAvailabilityPageProps) {
|
export default function Type(props: DynamicAvailabilityPageProps) {
|
||||||
return <AvailabilityPage {...props} />;
|
return <AvailabilityPage {...props} />;
|
||||||
@@ -48,11 +49,15 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
if (!userId)
|
if (!userId)
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
if (hashedLink?.eventType.slug !== slug)
|
if (hashedLink?.eventType.slug !== slug)
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
|
|
||||||
const users = await prisma.user.findMany({
|
const users = await prisma.user.findMany({
|
||||||
@@ -92,6 +97,8 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
if (!users || !users.length) {
|
if (!users || !users.length) {
|
||||||
return {
|
return {
|
||||||
notFound: true,
|
notFound: true,
|
||||||
|
} as {
|
||||||
|
notFound: true;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,7 @@
|
|||||||
|
import withEmbedSsr from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import { getServerSideProps as _getServerSideProps } from "../[slug]";
|
||||||
|
|
||||||
|
export { default } from "../[slug]";
|
||||||
|
|
||||||
|
export const getServerSideProps = withEmbedSsr(_getServerSideProps);
|
||||||
@@ -2,7 +2,6 @@ import { GetServerSidePropsContext } from "next";
|
|||||||
import { JSONObject } from "superjson/dist/types";
|
import { JSONObject } from "superjson/dist/types";
|
||||||
|
|
||||||
import { parseRecurringEvent } from "@calcom/lib";
|
import { parseRecurringEvent } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
import { bookEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
|
|
||||||
@@ -124,6 +123,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
isDynamicGroupBooking: false,
|
isDynamicGroupBooking: false,
|
||||||
hasHashedBookingLink: true,
|
hasHashedBookingLink: true,
|
||||||
hashedLink: link,
|
hashedLink: link,
|
||||||
|
isEmbed: typeof context.query.embed === "string",
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
/* eslint-disable @typescript-eslint/no-empty-function */
|
/* eslint-disable @typescript-eslint/no-empty-function */
|
||||||
import autoAnimate from "@formkit/auto-animate";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
import { EventTypeCustomInput, PeriodType, Prisma, SchedulingType } from "@prisma/client";
|
||||||
import { GetServerSidePropsContext } from "next";
|
import { GetServerSidePropsContext } from "next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
@@ -69,6 +69,7 @@ export type FormValues = {
|
|||||||
periodCountCalendarDays: "1" | "0";
|
periodCountCalendarDays: "1" | "0";
|
||||||
periodDates: { startDate: Date; endDate: Date };
|
periodDates: { startDate: Date; endDate: Date };
|
||||||
seatsPerTimeSlot: number | null;
|
seatsPerTimeSlot: number | null;
|
||||||
|
seatsShowAttendees: boolean | null;
|
||||||
seatsPerTimeSlotEnabled: boolean;
|
seatsPerTimeSlotEnabled: boolean;
|
||||||
minimumBookingNotice: number;
|
minimumBookingNotice: number;
|
||||||
beforeBufferTime: number;
|
beforeBufferTime: number;
|
||||||
@@ -105,13 +106,11 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
|
// TODO: It isn't a good idea to maintain state using setEventType. If we want to connect the SSR'd data to tRPC, we should useQuery(["viewer.eventTypes.get"]) with initialData
|
||||||
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
|
// Due to this change, when Form is saved, there is no way to propagate that info to eventType (e.g. disabling stripe app doesn't allow recurring tab to be enabled without refresh).
|
||||||
const [eventType, setEventType] = useState(dbEventType);
|
const [eventType, setEventType] = useState(dbEventType);
|
||||||
const animationParentRef = useRef(null);
|
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { tabName } = querySchema.parse(router.query);
|
const { tabName } = querySchema.parse(router.query);
|
||||||
|
|
||||||
useEffect(() => {
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
animationParentRef.current && autoAnimate(animationParentRef.current);
|
|
||||||
}, [animationParentRef]);
|
|
||||||
|
|
||||||
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
|
const updateMutation = trpc.useMutation("viewer.eventTypes.update", {
|
||||||
onSuccess: async ({ eventType: newEventType }) => {
|
onSuccess: async ({ eventType: newEventType }) => {
|
||||||
@@ -232,6 +231,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
beforeBufferTime,
|
beforeBufferTime,
|
||||||
afterBufferTime,
|
afterBufferTime,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees,
|
||||||
bookingLimits,
|
bookingLimits,
|
||||||
recurringEvent,
|
recurringEvent,
|
||||||
locations,
|
locations,
|
||||||
@@ -259,6 +259,7 @@ const EventTypePage = (props: inferSSRProps<typeof getServerSideProps>) => {
|
|||||||
afterEventBuffer: afterBufferTime,
|
afterEventBuffer: afterBufferTime,
|
||||||
bookingLimits,
|
bookingLimits,
|
||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees,
|
||||||
metadata,
|
metadata,
|
||||||
});
|
});
|
||||||
}}>
|
}}>
|
||||||
@@ -391,6 +392,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
price: true,
|
price: true,
|
||||||
destinationCalendar: true,
|
destinationCalendar: true,
|
||||||
seatsPerTimeSlot: true,
|
seatsPerTimeSlot: true,
|
||||||
|
seatsShowAttendees: true,
|
||||||
workflows: {
|
workflows: {
|
||||||
include: {
|
include: {
|
||||||
workflow: {
|
workflow: {
|
||||||
@@ -458,7 +460,7 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
|
|
||||||
const eventType = {
|
const eventType = {
|
||||||
...restEventType,
|
...restEventType,
|
||||||
schedule: rawEventType.schedule?.id || rawEventType.users[0].defaultScheduleId,
|
schedule: rawEventType.schedule?.id || rawEventType.users[0]?.defaultScheduleId || null,
|
||||||
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
|
recurringEvent: parseRecurringEvent(restEventType.recurringEvent),
|
||||||
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
bookingLimits: parseBookingLimit(restEventType.bookingLimits),
|
||||||
locations: locations as unknown as LocationObject[],
|
locations: locations as unknown as LocationObject[],
|
||||||
|
|||||||
@@ -98,13 +98,34 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
});
|
});
|
||||||
|
|
||||||
const setHiddenMutation = trpc.useMutation("viewer.eventTypes.update", {
|
const setHiddenMutation = trpc.useMutation("viewer.eventTypes.update", {
|
||||||
onError: async (err) => {
|
onMutate: async ({ id }) => {
|
||||||
console.error(err.message);
|
|
||||||
await utils.cancelQuery(["viewer.eventTypes"]);
|
await utils.cancelQuery(["viewer.eventTypes"]);
|
||||||
await utils.invalidateQueries(["viewer.eventTypes"]);
|
const previousValue = utils.getQueryData(["viewer.eventTypes"]);
|
||||||
|
if (previousValue) {
|
||||||
|
const newList = [...types];
|
||||||
|
const itemIndex = newList.findIndex((item) => item.id === id);
|
||||||
|
if (itemIndex !== -1 && newList[itemIndex]) {
|
||||||
|
newList[itemIndex].hidden = !newList[itemIndex].hidden;
|
||||||
|
}
|
||||||
|
utils.setQueryData(["viewer.eventTypes"], {
|
||||||
|
...previousValue,
|
||||||
|
eventTypeGroups: [
|
||||||
|
...previousValue.eventTypeGroups.slice(0, groupIndex),
|
||||||
|
{ ...group, eventTypes: newList },
|
||||||
|
...previousValue.eventTypeGroups.slice(groupIndex + 1),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { previousValue };
|
||||||
},
|
},
|
||||||
onSettled: async () => {
|
onError: async (err, _, context) => {
|
||||||
await utils.invalidateQueries(["viewer.eventTypes"]);
|
if (context?.previousValue) {
|
||||||
|
utils.setQueryData(["viewer.eventTypes"], context.previousValue);
|
||||||
|
}
|
||||||
|
console.error(err.message);
|
||||||
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.invalidateQueries(["viewer.eventTypes"]);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -169,12 +190,31 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
};
|
};
|
||||||
|
|
||||||
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
|
const deleteMutation = trpc.useMutation("viewer.eventTypes.delete", {
|
||||||
onSuccess: async () => {
|
onSuccess: () => {
|
||||||
await utils.invalidateQueries(["viewer.eventTypes"]);
|
|
||||||
showToast(t("event_type_deleted_successfully"), "success");
|
showToast(t("event_type_deleted_successfully"), "success");
|
||||||
setDeleteDialogOpen(false);
|
setDeleteDialogOpen(false);
|
||||||
},
|
},
|
||||||
onError: (err) => {
|
onMutate: async ({ id }) => {
|
||||||
|
await utils.cancelQuery(["viewer.eventTypes"]);
|
||||||
|
const previousValue = utils.getQueryData(["viewer.eventTypes"]);
|
||||||
|
if (previousValue) {
|
||||||
|
const newList = types.filter((item) => item.id !== id);
|
||||||
|
|
||||||
|
utils.setQueryData(["viewer.eventTypes"], {
|
||||||
|
...previousValue,
|
||||||
|
eventTypeGroups: [
|
||||||
|
...previousValue.eventTypeGroups.slice(0, groupIndex),
|
||||||
|
{ ...group, eventTypes: newList },
|
||||||
|
...previousValue.eventTypeGroups.slice(groupIndex + 1),
|
||||||
|
],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return { previousValue };
|
||||||
|
},
|
||||||
|
onError: (err, _, context) => {
|
||||||
|
if (context?.previousValue) {
|
||||||
|
utils.setQueryData(["viewer.eventTypes"], context.previousValue);
|
||||||
|
}
|
||||||
if (err instanceof HttpError) {
|
if (err instanceof HttpError) {
|
||||||
const message = `${err.statusCode}: ${err.message}`;
|
const message = `${err.statusCode}: ${err.message}`;
|
||||||
showToast(message, "error");
|
showToast(message, "error");
|
||||||
@@ -183,6 +223,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
showToast(err.message, "error");
|
showToast(err.message, "error");
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
onSettled: () => {
|
||||||
|
utils.invalidateQueries(["viewer.eventTypes"]);
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const [isNativeShare, setNativeShare] = useState(true);
|
const [isNativeShare, setNativeShare] = useState(true);
|
||||||
@@ -279,7 +322,10 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
/>
|
/>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
<DropdownMenuTrigger
|
||||||
|
asChild
|
||||||
|
data-testid={"event-type-options-" + type.id}
|
||||||
|
className="radix-state-open:rounded-r-md">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
size="icon"
|
size="icon"
|
||||||
@@ -439,7 +485,6 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
</ul>
|
</ul>
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_event_type")}
|
title={t("delete_event_type")}
|
||||||
confirmBtnText={t("confirm_delete_event_type")}
|
confirmBtnText={t("confirm_delete_event_type")}
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ import { DEFAULT_SCHEDULE } from "@calcom/lib/availability";
|
|||||||
import { DOCS_URL } from "@calcom/lib/constants";
|
import { DOCS_URL } from "@calcom/lib/constants";
|
||||||
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
import { fetchUsername } from "@calcom/lib/fetchUsername";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import showToast from "@calcom/lib/notification";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
import { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||||
@@ -29,6 +28,7 @@ import Button from "@calcom/ui/Button";
|
|||||||
import { Icon } from "@calcom/ui/Icon";
|
import { Icon } from "@calcom/ui/Icon";
|
||||||
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
import TimezoneSelect from "@calcom/ui/form/TimezoneSelect";
|
||||||
import { Form } from "@calcom/ui/form/fields";
|
import { Form } from "@calcom/ui/form/fields";
|
||||||
|
import showToast from "@calcom/ui/v2/core/notifications";
|
||||||
|
|
||||||
import { getSession } from "@lib/auth";
|
import { getSession } from "@lib/auth";
|
||||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Label, Input, TextField } from "@calcom/ui/form/fields";
|
import { Label, Input, TextField } from "@calcom/ui/form/fields";
|
||||||
|
import { Segment, SegmentOption } from "@calcom/ui/v2/core";
|
||||||
|
|
||||||
import { sandboxPage } from ".";
|
import { sandboxPage } from ".";
|
||||||
|
|
||||||
@@ -14,6 +15,17 @@ const page = sandboxPage(() => (
|
|||||||
</div>
|
</div>
|
||||||
<div>
|
<div>
|
||||||
<TextField name="test-02" label="TextField" placeholder="it has an input baked in" />
|
<TextField name="test-02" label="TextField" placeholder="it has an input baked in" />
|
||||||
|
{/* Adding to sandbox cause storybook doesnt like radix tailwind :S */}
|
||||||
|
<div className="pt-4">
|
||||||
|
<Segment label="Test" value="Test">
|
||||||
|
<SegmentOption value="Test">One</SegmentOption>
|
||||||
|
<SegmentOption value="2">Two</SegmentOption>
|
||||||
|
<SegmentOption value="3">Three</SegmentOption>
|
||||||
|
<SegmentOption value="4" disabled>
|
||||||
|
Four
|
||||||
|
</SegmentOption>
|
||||||
|
</Segment>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
));
|
));
|
||||||
|
|||||||
@@ -3,18 +3,17 @@ import Link from "next/link";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { Fragment } from "react";
|
import { Fragment } from "react";
|
||||||
|
|
||||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Icon } from "@calcom/ui";
|
import { Icon } from "@calcom/ui";
|
||||||
import { Alert } from "@calcom/ui/v2";
|
import { Alert, Button } from "@calcom/ui/v2";
|
||||||
import Badge from "@calcom/ui/v2/core/Badge";
|
import Badge from "@calcom/ui/v2/core/Badge";
|
||||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||||
import Meta from "@calcom/ui/v2/core/Meta";
|
import Meta from "@calcom/ui/v2/core/Meta";
|
||||||
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
|
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
|
||||||
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
|
import { SkeletonContainer, SkeletonText, SkeletonButton } from "@calcom/ui/v2/core/skeleton";
|
||||||
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
|
import { List, ListItem, ListItemText, ListItemTitle } from "@calcom/ui/v2/modules/List";
|
||||||
import DestinationCalendarSelector from "@calcom/ui/v2/modules/event-types/DestinationCalendarSelector";
|
|
||||||
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
import DisconnectIntegration from "@calcom/ui/v2/modules/integrations/DisconnectIntegration";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
@@ -36,6 +35,18 @@ const SkeletonLoader = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const AddCalendarButton = () => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Button color="secondary" StartIcon={Icon.FiPlus} href="/apps/categories/calendar">
|
||||||
|
{t("add_calendar")}
|
||||||
|
</Button>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CalendarsView = () => {
|
const CalendarsView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -51,7 +62,11 @@ const CalendarsView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Meta title="Calendars" description="Configure how your event types interact with your calendars" />
|
<Meta
|
||||||
|
title="Calendars"
|
||||||
|
description="Configure how your event types interact with your calendars"
|
||||||
|
CTA={<AddCalendarButton />}
|
||||||
|
/>
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
customLoader={<SkeletonLoader />}
|
customLoader={<SkeletonLoader />}
|
||||||
@@ -63,7 +78,7 @@ const CalendarsView = () => {
|
|||||||
<Icon.FiCalendar className="h-6 w-6" />
|
<Icon.FiCalendar className="h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex flex-col space-y-3">
|
<div className="flex w-full flex-col space-y-3">
|
||||||
<div>
|
<div>
|
||||||
<h4 className=" pb-2 text-base font-semibold leading-5 text-black">
|
<h4 className=" pb-2 text-base font-semibold leading-5 text-black">
|
||||||
{t("add_to_calendar")}
|
{t("add_to_calendar")}
|
||||||
@@ -174,7 +189,7 @@ const CalendarsView = () => {
|
|||||||
headline={t("no_calendar_installed")}
|
headline={t("no_calendar_installed")}
|
||||||
description={t("no_calendar_installed_description")}
|
description={t("no_calendar_installed_description")}
|
||||||
buttonText={t("add_a_calendar")}
|
buttonText={t("add_a_calendar")}
|
||||||
buttonOnClick={() => router.push(`${WEBAPP_URL}/apps/categories/calendar`)}
|
buttonOnClick={() => router.push("/apps/categories/calendar")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
|
|||||||
Loaded 100 of 402 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user