Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
36d47bd2d8 | ||
|
|
26cdb6521f | ||
|
|
014f1986de | ||
|
|
3ef328421f | ||
|
|
c4fe69d826 | ||
|
|
6fd64b2209 | ||
|
|
585e36793a | ||
|
|
09b9370e13 | ||
|
|
a76a4996b4 | ||
|
|
b28d60b6dc | ||
|
|
a716eda14c | ||
|
|
99f1524183 | ||
|
|
7ce6454915 | ||
|
|
93cb21f55a | ||
|
|
0aca769d06 | ||
|
|
c0bda6257e | ||
|
|
ae22246f05 | ||
|
|
e2931d9fa2 | ||
|
|
e6a8a31b53 | ||
|
|
ee86c751f7 | ||
|
|
073b212c05 | ||
|
|
cd8e2124c4 | ||
|
|
9dd36bd20e | ||
|
|
b1404356ff | ||
|
|
f467bb7957 | ||
|
|
597b14cf4c | ||
|
|
2bdd6df755 | ||
|
|
ad958f10bd | ||
|
|
e5eb7c3906 | ||
|
|
667d42d5da | ||
|
|
a23a9ad15a | ||
|
|
4482ffc66b | ||
|
|
493efabd2e | ||
|
|
a0355e276d | ||
|
|
b5c993e153 | ||
|
|
da3ee3f1fe | ||
|
|
d9fc595444 | ||
|
|
06d13e0f6e | ||
|
|
7dc3266133 | ||
|
|
2c96444058 | ||
|
|
39c48e7f79 | ||
|
|
b6e1989f17 | ||
|
|
45b5b2a376 | ||
|
|
05726ec94c | ||
|
|
6340ff43d8 | ||
|
|
e64b26c7e1 | ||
|
|
c829a7de82 | ||
|
|
0205bfb066 | ||
|
|
5fd7d63388 | ||
|
|
e1ea2cb532 | ||
|
|
88d151aa35 | ||
|
|
4e7c93a322 | ||
|
|
281c7628c1 | ||
|
|
1b24bc14cd | ||
|
|
be3d2fca05 | ||
|
|
327e11e7c7 | ||
|
|
bd999345f4 | ||
|
|
081776b4f9 | ||
|
|
8c328ce5d7 | ||
|
|
0fa2a6aea3 | ||
|
|
440251f9c3 | ||
|
|
7f6847f7c7 | ||
|
|
9f1fa83a97 | ||
|
|
f00b112792 | ||
|
|
5339a489f5 | ||
|
|
f7df204e0b | ||
|
|
4975aba5e0 | ||
|
|
29d61ad67b | ||
|
|
6e555e64fa | ||
|
|
c748bbc7e6 | ||
|
|
0681359bf3 | ||
|
|
4c9d4d7972 | ||
|
|
67f8de42b3 | ||
|
|
87584132b5 | ||
|
|
5b943742e0 | ||
|
|
fe155b0b97 | ||
|
|
6639588c2d | ||
|
|
7f9e8ca7a6 | ||
|
|
e478a46358 | ||
|
|
7c9012738a | ||
|
|
5c763389f7 | ||
|
|
7fde30edf9 | ||
|
|
0e22e92cab | ||
|
|
17f48614ff | ||
|
|
2f33b57eb7 | ||
|
|
6e5c3a88ee | ||
|
|
9024914f23 | ||
|
|
966bc08eb4 | ||
|
|
c545331cfe | ||
|
|
ce763f30f6 | ||
|
|
d453ae9f2b | ||
|
|
9f2b41e597 | ||
|
|
86e903ce4a | ||
|
|
9d3963436b | ||
|
|
d8218a6243 | ||
|
|
cdc3833e09 | ||
|
|
52b7e401a1 | ||
|
|
5170fc2424 | ||
|
|
7349fb9f6d | ||
|
|
a8368aac79 | ||
|
|
91f381bce9 | ||
|
|
fb9c1b18e0 | ||
|
|
1d18a09cc8 | ||
|
|
887d63ceef | ||
|
|
555fcc21f2 | ||
|
|
f45fb03838 | ||
|
|
46611fdf77 | ||
|
|
c0f7c40bec | ||
|
|
f6f257c705 | ||
|
|
d61f50724b | ||
|
|
960aa0e00b | ||
|
|
817e20f11e | ||
|
|
af0123a606 | ||
|
|
709bbf6626 | ||
|
|
53097ee3bc | ||
|
|
31f2002654 | ||
|
|
ee155c3643 | ||
|
|
8ab20c3c9f | ||
|
|
4f6d035d29 | ||
|
|
2e5b42ef47 | ||
|
|
b055672ea0 | ||
|
|
e4fbca388c | ||
|
|
2b8bba09c9 | ||
|
|
98cf29f655 | ||
|
|
ae8706316f | ||
|
|
ec8e08d442 | ||
|
|
c33f424333 | ||
|
|
6f2312d1e2 |
No files matched your search
+2
-3
@@ -160,9 +160,6 @@ CLOSECOM_API_KEY=
|
|||||||
# Sendgrid internal sync service
|
# Sendgrid internal sync service
|
||||||
SENDGRID_SYNC_API_KEY=
|
SENDGRID_SYNC_API_KEY=
|
||||||
|
|
||||||
# Sentry
|
|
||||||
NEXT_PUBLIC_SENTRY_DSN=
|
|
||||||
SENTRY_IGNORE_API_RESOLUTION_ERROR=
|
|
||||||
|
|
||||||
# Change your Brand
|
# Change your Brand
|
||||||
NEXT_PUBLIC_APP_NAME="Cal.com"
|
NEXT_PUBLIC_APP_NAME="Cal.com"
|
||||||
@@ -177,3 +174,5 @@ CSP_POLICY=
|
|||||||
|
|
||||||
# Vercel Edge Config
|
# Vercel Edge Config
|
||||||
EDGE_CONFIG=
|
EDGE_CONFIG=
|
||||||
|
|
||||||
|
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||||
+2
-1
@@ -5,7 +5,8 @@
|
|||||||
# the repo. Unless a later match takes precedence,
|
# the repo. Unless a later match takes precedence,
|
||||||
# @global-owner1 and @global-owner2 will be requested for
|
# @global-owner1 and @global-owner2 will be requested for
|
||||||
# review when someone opens a pull request.
|
# review when someone opens a pull request.
|
||||||
* @calcom/reviewers
|
# ⬇️ Removed so we can manually trigger checks
|
||||||
|
# * @calcom/reviewers
|
||||||
|
|
||||||
.github/ @calcom/core
|
.github/ @calcom/core
|
||||||
.husky/ @calcom/core
|
.husky/ @calcom/core
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ jobs:
|
|||||||
steps:
|
steps:
|
||||||
- uses: actions/stale@v7
|
- uses: actions/stale@v7
|
||||||
with:
|
with:
|
||||||
days-before-stale: 60
|
days-before-issue-stale: 60
|
||||||
include-only-assigned: true
|
days-before-issue-close: -1
|
||||||
days-before-close: -1
|
days-before-pr-stale: 14
|
||||||
|
days-before-pr-close: 7
|
||||||
|
stale-pr-message: "This PR is being marked as stale due to inactivity."
|
||||||
|
close-pr-message: "This PR is being closed due to inactivity. Please reopen if work is intended to be continued."
|
||||||
@@ -68,5 +68,3 @@ public/embed
|
|||||||
|
|
||||||
# Copied app-store images
|
# Copied app-store images
|
||||||
public/app-store
|
public/app-store
|
||||||
# Sentry
|
|
||||||
.sentryclirc
|
|
||||||
@@ -103,59 +103,6 @@ paths:
|
|||||||
summary: Reset a user's password
|
summary: Reset a user's password
|
||||||
tags:
|
tags:
|
||||||
- Authentication
|
- Authentication
|
||||||
"/api/availability/{user}?dateFrom={dateFrom}&dateTo={dateTo}":
|
|
||||||
get:
|
|
||||||
description: "Gets the busy times for a particular user, by username."
|
|
||||||
summary: Gets the busy times for a user
|
|
||||||
tags:
|
|
||||||
- Availability
|
|
||||||
responses:
|
|
||||||
"200":
|
|
||||||
description: OK
|
|
||||||
content:
|
|
||||||
application/json:
|
|
||||||
schema:
|
|
||||||
type: array
|
|
||||||
description: ""
|
|
||||||
minItems: 1
|
|
||||||
uniqueItems: true
|
|
||||||
x-examples:
|
|
||||||
example-1:
|
|
||||||
- start: "Fri, 03 Sep 2021 17:00:00 GMT"
|
|
||||||
end: "Fri, 03 Sep 2021 17:40:00 GMT"
|
|
||||||
items:
|
|
||||||
type: object
|
|
||||||
properties:
|
|
||||||
start:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
end:
|
|
||||||
type: string
|
|
||||||
minLength: 1
|
|
||||||
required:
|
|
||||||
- start
|
|
||||||
- end
|
|
||||||
"500":
|
|
||||||
description: Internal Server Error
|
|
||||||
parameters:
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: user
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The username of who you want to check availability for
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateFrom
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times from
|
|
||||||
- schema:
|
|
||||||
type: string
|
|
||||||
name: dateTo
|
|
||||||
in: path
|
|
||||||
required: true
|
|
||||||
description: The timestamp of which time you want to get busy times until
|
|
||||||
/api/availability/calendar:
|
/api/availability/calendar:
|
||||||
get:
|
get:
|
||||||
description: Gets the user's selected calendars.
|
description: Gets the user's selected calendars.
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function AddToHomescreen() {
|
export default function AddToHomescreen() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
|||||||
type="button"
|
type="button"
|
||||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||||
<span className="sr-only">{t("dismiss")}</span>
|
<span className="sr-only">{t("dismiss")}</span>
|
||||||
<FiX className="text-inverted h-6 w-6" aria-hidden="true" />
|
<X className="text-inverted h-6 w-6" aria-hidden="true" />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { Badge, ListItemText } from "@calcom/ui";
|
import { Badge, ListItemText } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
type ShouldHighlight = { slug: string; shouldHighlight: true } | { shouldHighlight?: never; slug?: never };
|
||||||
|
|
||||||
@@ -77,7 +77,7 @@ export default function AppListCard(props: AppListCardProps) {
|
|||||||
<ListItemText component="p">{description}</ListItemText>
|
<ListItemText component="p">{description}</ListItemText>
|
||||||
{invalidCredential && (
|
{invalidCredential && (
|
||||||
<div className="flex gap-x-2 pt-2">
|
<div className="flex gap-x-2 pt-2">
|
||||||
<FiAlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
<AlertCircle className="h-8 w-8 text-red-500 sm:h-4 sm:w-4" />
|
||||||
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
<ListItemText component="p" className="whitespace-pre-wrap text-red-500">
|
||||||
{t("invalid_credential")}
|
{t("invalid_credential")}
|
||||||
</ListItemText>
|
</ListItemText>
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ import {
|
|||||||
TextField,
|
TextField,
|
||||||
ColorPicker,
|
ColorPicker,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCode, FiTrello, FiSun, FiArrowLeft } from "@calcom/ui/components/icon";
|
import { Code, Trello, Sun, ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -493,7 +493,7 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "HTML",
|
name: "HTML",
|
||||||
href: "embedTabName=embed-code",
|
href: "embedTabName=embed-code",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -546,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
|||||||
{
|
{
|
||||||
name: "React",
|
name: "React",
|
||||||
href: "embedTabName=embed-react",
|
href: "embedTabName=embed-react",
|
||||||
icon: FiCode,
|
icon: Code,
|
||||||
type: "code",
|
type: "code",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||||
@@ -586,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
|||||||
{
|
{
|
||||||
name: "Preview",
|
name: "Preview",
|
||||||
href: "embedTabName=embed-preview",
|
href: "embedTabName=embed-preview",
|
||||||
icon: FiTrello,
|
icon: Trello,
|
||||||
type: "iframe",
|
type: "iframe",
|
||||||
Component: forwardRef<
|
Component: forwardRef<
|
||||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||||
@@ -622,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
|||||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||||
return (
|
return (
|
||||||
<components.Control {...props}>
|
<components.Control {...props}>
|
||||||
<FiSun className="text-subtle ml-2 h-4 w-4" />
|
<Sun className="text-subtle ml-2 h-4 w-4" />
|
||||||
{children}
|
{children}
|
||||||
</components.Control>
|
</components.Control>
|
||||||
);
|
);
|
||||||
@@ -830,7 +830,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||||
}}>
|
}}>
|
||||||
<FiArrowLeft className="mr-4 w-4" />
|
<ArrowLeft className="mr-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
{embed.title}
|
{embed.title}
|
||||||
</h3>
|
</h3>
|
||||||
|
|||||||
@@ -0,0 +1,99 @@
|
|||||||
|
import { DefaultSeo } from "next-seo";
|
||||||
|
import { Inter } from "next/font/google";
|
||||||
|
import localFont from "next/font/local";
|
||||||
|
import Script from "next/script";
|
||||||
|
|
||||||
|
import "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import { useEmbedUiConfig } from "@calcom/embed-core/src/embed-iframe";
|
||||||
|
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
||||||
|
|
||||||
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
import AppProviders from "@lib/app-providers";
|
||||||
|
import { seoConfig } from "@lib/config/next-seo.config";
|
||||||
|
|
||||||
|
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
||||||
|
|
||||||
|
export interface CalPageWrapper {
|
||||||
|
(props?: any): JSX.Element;
|
||||||
|
PageWrapper?: AppProps["Component"]["PageWrapper"];
|
||||||
|
}
|
||||||
|
|
||||||
|
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
||||||
|
const calFont = localFont({
|
||||||
|
src: "../fonts/CalSans-SemiBold.woff2",
|
||||||
|
variable: "--font-cal",
|
||||||
|
preload: true,
|
||||||
|
display: "swap",
|
||||||
|
});
|
||||||
|
|
||||||
|
function PageWrapper(props: AppProps) {
|
||||||
|
const { Component, pageProps, err, router } = props;
|
||||||
|
let pageStatus = "200";
|
||||||
|
const { cssVarsPerTheme } = useEmbedUiConfig();
|
||||||
|
const cssVarsStyle = [];
|
||||||
|
if (cssVarsPerTheme) {
|
||||||
|
for (const [themeName, cssVars] of Object.entries(cssVarsPerTheme)) {
|
||||||
|
cssVarsStyle.push(`.${themeName} {`);
|
||||||
|
for (const [cssVarName, value] of Object.entries(cssVars)) {
|
||||||
|
cssVarsStyle.push(`--${cssVarName}: ${value};`);
|
||||||
|
}
|
||||||
|
cssVarsStyle.push(`}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (router.pathname === "/404") {
|
||||||
|
pageStatus = "404";
|
||||||
|
} else if (router.pathname === "/500") {
|
||||||
|
pageStatus = "500";
|
||||||
|
}
|
||||||
|
|
||||||
|
// On client side don't let nonce creep into DOM
|
||||||
|
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
||||||
|
// See https://github.com/kentcdodds/nonce-hydration-issues
|
||||||
|
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
||||||
|
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
||||||
|
const providerProps = {
|
||||||
|
...props,
|
||||||
|
pageProps: {
|
||||||
|
...props.pageProps,
|
||||||
|
nonce,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
// Use the layout defined at the page level, if available
|
||||||
|
const getLayout = Component.getLayout ?? ((page) => page);
|
||||||
|
return (
|
||||||
|
<AppProviders {...providerProps}>
|
||||||
|
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
||||||
|
<I18nLanguageHandler />
|
||||||
|
<Script
|
||||||
|
nonce={nonce}
|
||||||
|
id="page-status"
|
||||||
|
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
||||||
|
/>
|
||||||
|
<style jsx global>{`
|
||||||
|
:root {
|
||||||
|
--font-inter: ${interFont.style.fontFamily};
|
||||||
|
--font-cal: ${calFont.style.fontFamily};
|
||||||
|
}
|
||||||
|
`}</style>
|
||||||
|
|
||||||
|
<style jsx global>
|
||||||
|
{`
|
||||||
|
${cssVarsStyle.join("")}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
{getLayout(
|
||||||
|
Component.requiresLicense ? (
|
||||||
|
<LicenseRequired>
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
</LicenseRequired>
|
||||||
|
) : (
|
||||||
|
<Component {...pageProps} err={err} />
|
||||||
|
),
|
||||||
|
router
|
||||||
|
)}
|
||||||
|
</AppProviders>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
export default PageWrapper;
|
||||||
@@ -3,7 +3,7 @@ import React from "react";
|
|||||||
|
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { ErrorBoundary } from "@calcom/ui";
|
import { ErrorBoundary } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiKey, FiLock, FiTerminal, FiUser, FiUsers } from "@calcom/ui/components/icon";
|
import { CreditCard, Key, Lock, Terminal, User, Users } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import NavTabs from "./NavTabs";
|
import NavTabs from "./NavTabs";
|
||||||
|
|
||||||
@@ -11,32 +11,32 @@ const tabs = [
|
|||||||
{
|
{
|
||||||
name: "profile",
|
name: "profile",
|
||||||
href: "/settings/my-account/profile",
|
href: "/settings/my-account/profile",
|
||||||
icon: FiUser,
|
icon: User,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "teams",
|
name: "teams",
|
||||||
href: "/settings/teams",
|
href: "/settings/teams",
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "security",
|
name: "security",
|
||||||
href: "/settings/security",
|
href: "/settings/security",
|
||||||
icon: FiKey,
|
icon: Key,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "developer",
|
name: "developer",
|
||||||
href: "/settings/developer",
|
href: "/settings/developer",
|
||||||
icon: FiTerminal,
|
icon: Terminal,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "billing",
|
name: "billing",
|
||||||
href: "/settings/billing",
|
href: "/settings/billing",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "admin",
|
name: "admin",
|
||||||
href: "/settings/admin",
|
href: "/settings/admin",
|
||||||
icon: FiLock,
|
icon: Lock,
|
||||||
adminRequired: true,
|
adminRequired: true,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import {
|
|||||||
DropdownMenuItem,
|
DropdownMenuItem,
|
||||||
DropdownItem,
|
DropdownItem,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -40,7 +40,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
return (
|
return (
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
@@ -48,7 +48,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
{options.map((data) => (
|
{options.map((data) => (
|
||||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||||
{data.slug === "add-new" ? (
|
{data.slug === "add-new" ? (
|
||||||
<DropdownItem StartIcon={FiPlus} color="minimal" href="/apps/categories/calendar">
|
<DropdownItem StartIcon={Plus} color="minimal" href="/apps/categories/calendar">
|
||||||
{t("install_new_calendar_app")}
|
{t("install_new_calendar_app")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -14,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { App as AppType } from "@calcom/types/App";
|
import type { App as AppType } from "@calcom/types/App";
|
||||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||||
import {
|
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
||||||
FiBookOpen,
|
|
||||||
FiCheck,
|
|
||||||
FiExternalLink,
|
|
||||||
FiFile,
|
|
||||||
FiFlag,
|
|
||||||
FiMail,
|
|
||||||
FiPlus,
|
|
||||||
FiShield,
|
|
||||||
} from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
/* These app slugs all require Google Cal to be installed */
|
/* These app slugs all require Google Cal to be installed */
|
||||||
|
|
||||||
@@ -153,7 +144,7 @@ const Component = ({
|
|||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
<Button StartIcon={Check} color="secondary" disabled>
|
||||||
{existingCredentials.length > 0
|
{existingCredentials.length > 0
|
||||||
? t("active_install", { count: existingCredentials.length })
|
? t("active_install", { count: existingCredentials.length })
|
||||||
: t("default")}
|
: t("default")}
|
||||||
@@ -175,7 +166,7 @@ const Component = ({
|
|||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
{...props}
|
{...props}
|
||||||
// @TODO: Overriding color and size prevent us from
|
// @TODO: Overriding color and size prevent us from
|
||||||
// having to duplicate InstallAppButton for now.
|
// having to duplicate InstallAppButton for now.
|
||||||
@@ -247,7 +238,7 @@ const Component = ({
|
|||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<div className="prose-sm prose prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
<div className="prose-sm prose prose-a:text-default prose-headings:text-emphasis prose-code:text-default prose-strong:text-default text-default mt-8">
|
||||||
{body}
|
{body}
|
||||||
</div>
|
</div>
|
||||||
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
<h4 className="text-emphasis mt-8 font-semibold ">{t("pricing")}</h4>
|
||||||
@@ -275,7 +266,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||||
href={docs}>
|
href={docs}>
|
||||||
<FiBookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
<BookOpen className="text-subtle mr-1 -mt-1 inline h-4 w-4" />
|
||||||
{t("documentation")}
|
{t("documentation")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -287,7 +278,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={website}>
|
href={website}>
|
||||||
<FiExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<ExternalLink className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{website.replace("https://", "")}
|
{website.replace("https://", "")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -299,7 +290,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={"mailto:" + email}>
|
href={"mailto:" + email}>
|
||||||
<FiMail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<Mail className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
|
|
||||||
{email}
|
{email}
|
||||||
</a>
|
</a>
|
||||||
@@ -312,7 +303,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={tos}>
|
href={tos}>
|
||||||
<FiFile className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<File className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("terms_of_service")}
|
{t("terms_of_service")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -324,7 +315,7 @@ const Component = ({
|
|||||||
rel="noreferrer"
|
rel="noreferrer"
|
||||||
className="text-emphasis font-normal no-underline hover:underline"
|
className="text-emphasis font-normal no-underline hover:underline"
|
||||||
href={privacy}>
|
href={privacy}>
|
||||||
<FiShield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
<Shield className="text-subtle mr-1 -mt-px inline h-4 w-4" />
|
||||||
{t("privacy_policy")}
|
{t("privacy_policy")}
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -335,7 +326,7 @@ const Component = ({
|
|||||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||||
</span>
|
</span>
|
||||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
ShellSubHeading,
|
ShellSubHeading,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiArrowLeft, FiCalendar, FiPlus } from "@calcom/ui/components/icon";
|
import { ArrowLeft, Calendar, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{!!props.destination && (
|
{!!props.destination && (
|
||||||
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
|
<span className="bg-subtle text-default ml-8 inline-flex items-center gap-1 rounded-md px-2 py-1 text-sm font-normal sm:ml-4">
|
||||||
<FiArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
{t("adding_events_to")}
|
{t("adding_events_to")}
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -264,7 +264,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
{!!data.connectedCalendars.length || !!installedCalendars.data?.items.length ? (
|
||||||
<>
|
<>
|
||||||
{heading && (
|
{heading && (
|
||||||
<div className="flex flex-col gap-6 rounded-md border p-7">
|
<div className="border-default flex flex-col gap-6 rounded-md border p-7">
|
||||||
<ShellSubHeading
|
<ShellSubHeading
|
||||||
title={t("calendar")}
|
title={t("calendar")}
|
||||||
subtitle={t("installed_app_calendar_description")}
|
subtitle={t("installed_app_calendar_description")}
|
||||||
@@ -282,8 +282,8 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
<div className="bg-muted border-subtle flex justify-between rounded-md border p-4">
|
||||||
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
<div className="flex w-full flex-col items-start gap-4 md:flex-row md:items-center">
|
||||||
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
<div className="bg-default border-subtle relative rounded-md border p-1.5">
|
||||||
<FiCalendar className="text-default h-8 w-8" strokeWidth="1" />
|
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||||
<FiPlus
|
<Plus
|
||||||
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
||||||
strokeWidth="4"
|
strokeWidth="4"
|
||||||
/>
|
/>
|
||||||
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiCalendar}
|
Icon={Calendar}
|
||||||
headline={t("no_category_apps", {
|
headline={t("no_category_apps", {
|
||||||
category: t("calendar").toLowerCase(),
|
category: t("calendar").toLowerCase(),
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import React from "react";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmptyScreen } from "@calcom/ui";
|
import { EmptyScreen } from "@calcom/ui";
|
||||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type AppsLayoutProps = {
|
type AppsLayoutProps = {
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
|||||||
<main className="w-full">
|
<main className="w-full">
|
||||||
{emptyStore ? (
|
{emptyStore ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiAlertCircle}
|
Icon={AlertCircle}
|
||||||
headline={t("no_apps")}
|
headline={t("no_apps")}
|
||||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||||
|
|||||||
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps, VerticalTabItemProps } from "@calcom/ui";
|
||||||
import { FiBarChart, FiCalendar, FiCreditCard, FiGrid, FiShare2, FiVideo } from "@calcom/ui/components/icon";
|
import { BarChart, Calendar, CreditCard, Grid, Share2, Video } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
const tabs: (VerticalTabItemProps | HorizontalTabItemProps)[] = [
|
||||||
{
|
{
|
||||||
name: "calendar",
|
name: "calendar",
|
||||||
href: "/apps/installed/calendar",
|
href: "/apps/installed/calendar",
|
||||||
icon: FiCalendar,
|
icon: Calendar,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "conferencing",
|
name: "conferencing",
|
||||||
href: "/apps/installed/conferencing",
|
href: "/apps/installed/conferencing",
|
||||||
icon: FiVideo,
|
icon: Video,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "payment",
|
name: "payment",
|
||||||
href: "/apps/installed/payment",
|
href: "/apps/installed/payment",
|
||||||
icon: FiCreditCard,
|
icon: CreditCard,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "automation",
|
name: "automation",
|
||||||
href: "/apps/installed/automation",
|
href: "/apps/installed/automation",
|
||||||
icon: FiShare2,
|
icon: Share2,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "analytics",
|
name: "analytics",
|
||||||
href: "/apps/installed/analytics",
|
href: "/apps/installed/analytics",
|
||||||
icon: FiBarChart,
|
icon: BarChart,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "other",
|
name: "other",
|
||||||
href: "/apps/installed/other",
|
href: "/apps/installed/other",
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { Button, SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
import { FiMoreHorizontal } from "@calcom/ui/components/icon";
|
import { MoreHorizontal } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -31,7 +31,7 @@ function SkeletonItem() {
|
|||||||
type="button"
|
type="button"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiMoreHorizontal}
|
StartIcon={MoreHorizontal}
|
||||||
disabled
|
disabled
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
|||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiLink } from "@calcom/ui/components/icon";
|
import { Link } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { Props } from "./pages/AvailabilityPage";
|
import type { Props } from "./pages/AvailabilityPage";
|
||||||
|
|
||||||
@@ -41,7 +41,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
|||||||
return (
|
return (
|
||||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||||
{eventLocationType.iconUrl === "/link.svg" ? (
|
{eventLocationType.iconUrl === "/link.svg" ? (
|
||||||
<FiLink className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
|
<Link className="text-default min-h-4 min-w-4 ml-[2px] opacity-70 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
) : (
|
) : (
|
||||||
<img
|
<img
|
||||||
src={eventLocationType.iconUrl}
|
src={eventLocationType.iconUrl}
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { nameOfDay } from "@calcom/lib/weekday";
|
import { nameOfDay } from "@calcom/lib/weekday";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
|
|||||||
slots?: Slot[];
|
slots?: Slot[];
|
||||||
isLoading: boolean;
|
isLoading: boolean;
|
||||||
ethSignature?: string;
|
ethSignature?: string;
|
||||||
|
duration: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||||
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
seatsPerTimeSlot,
|
seatsPerTimeSlot,
|
||||||
bookingAttendees,
|
bookingAttendees,
|
||||||
ethSignature,
|
ethSignature,
|
||||||
|
duration,
|
||||||
}) => {
|
}) => {
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -63,6 +67,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
[isMobile]
|
[isMobile]
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const reserveSlot = (slot: Slot) => {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
slotUtcStartDate: slot.time,
|
||||||
|
eventTypeId,
|
||||||
|
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div ref={slotPickerRef}>
|
<div ref={slotPickerRef}>
|
||||||
{!!date ? (
|
{!!date ? (
|
||||||
@@ -150,6 +162,7 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
|||||||
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
|
" bg-default dark:bg-muted border-default hover:bg-subtle hover:border-brand-default text-emphasis mb-2 block rounded-md border py-2 text-sm font-medium",
|
||||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||||
)}
|
)}
|
||||||
|
onClick={() => reserveSlot(slot)}
|
||||||
data-testid="time">
|
data-testid="time">
|
||||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||||
{!!seatsPerTimeSlot && (
|
{!!seatsPerTimeSlot && (
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
|
|||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Badge } from "@calcom/ui";
|
import { Badge } from "@calcom/ui";
|
||||||
import { FiCheckSquare, FiClock } from "@calcom/ui/components/icon";
|
import { CheckSquare, Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
@@ -103,7 +103,7 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
{requiresConfirmation && (
|
{requiresConfirmation && (
|
||||||
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
|
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
|
||||||
<div>
|
<div>
|
||||||
<FiCheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<CheckSquare className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
</div>
|
</div>
|
||||||
{requiresConfirmationText}
|
{requiresConfirmationText}
|
||||||
</div>
|
</div>
|
||||||
@@ -117,9 +117,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
isBookingPage && "text-default",
|
isBookingPage && "text-default",
|
||||||
!eventType.metadata?.multipleDuration && "items-center"
|
!eventType.metadata?.multipleDuration && "items-center"
|
||||||
)}>
|
)}>
|
||||||
<FiClock
|
<Clock
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
isBookingPage && "mt-[2px]"
|
isBookingPage && "mt-[2px]"
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -132,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
|
|||||||
variant="gray"
|
variant="gray"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
duration === dur.toString()
|
duration === dur.toString()
|
||||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
? "bg-emphasis border-emphasis text-emphasis "
|
||||||
: "hover:bg-darkgray-200 dark:hover:bg-darkmodebrand hover:text-darkgray-900 dark:hover:text-darkmodebrandcontrast dark:bg-darkgray-200 bg-emphasis text-emphasis dark:text-inverted",
|
: "bg-subtle text-default border-transparent ",
|
||||||
"cursor-pointer"
|
"cursor-pointer border"
|
||||||
)}
|
)}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDuration(dur);
|
setDuration(dur);
|
||||||
|
|||||||
@@ -1,22 +1,35 @@
|
|||||||
|
import type { TFunction } from "next-i18next";
|
||||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||||
|
|
||||||
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
import getPaymentAppData from "@calcom/lib/getPaymentAppData";
|
||||||
import { FiCreditCard } from "@calcom/ui/components/icon";
|
import { CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const BookingDescriptionPayment = (props: { eventType: Parameters<typeof getPaymentAppData>[0] }) => {
|
const BookingDescriptionPayment = (props: {
|
||||||
|
eventType: Parameters<typeof getPaymentAppData>[0];
|
||||||
|
t: TFunction;
|
||||||
|
}) => {
|
||||||
const paymentAppData = getPaymentAppData(props.eventType);
|
const paymentAppData = getPaymentAppData(props.eventType);
|
||||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
<p className="text-bookinglight -ml-2 px-2 text-sm ">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>
|
||||||
value={paymentAppData.price / 100.0}
|
{props.t("no_show_fee_amount", {
|
||||||
style="currency"
|
amount: paymentAppData.price / 100.0,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
/>
|
})}
|
||||||
</IntlProvider>
|
</>
|
||||||
|
) : (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -28,10 +28,11 @@ import {
|
|||||||
TableActions,
|
TableActions,
|
||||||
TextAreaField,
|
TextAreaField,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiCheck, FiClock, FiMapPin, FiRefreshCcw, FiSend, FiSlash, FiX } from "@calcom/ui/components/icon";
|
import { Check, Clock, MapPin, RefreshCcw, Send, Slash, X, CreditCard } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import useMeQuery from "@lib/hooks/useMeQuery";
|
import useMeQuery from "@lib/hooks/useMeQuery";
|
||||||
|
|
||||||
|
import { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||||
|
|
||||||
@@ -56,7 +57,9 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||||
|
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||||
|
const cardCharged = booking?.payment[0]?.success;
|
||||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
if (data?.status === BookingStatus.REJECTED) {
|
if (data?.status === BookingStatus.REJECTED) {
|
||||||
@@ -113,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: FiSlash,
|
icon: Slash,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isLoading,
|
||||||
},
|
},
|
||||||
{
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
id: "confirm",
|
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
||||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
? [
|
||||||
onClick: () => {
|
{
|
||||||
bookingConfirm(true);
|
id: "confirm",
|
||||||
},
|
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||||
icon: FiCheck,
|
onClick: () => {
|
||||||
disabled: mutation.isLoading,
|
bookingConfirm(true);
|
||||||
color: "primary",
|
},
|
||||||
},
|
icon: Check,
|
||||||
|
disabled: mutation.isLoading,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
: []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const showRecordingActions: ActionType[] = [
|
const showRecordingActions: ActionType[] = [
|
||||||
@@ -149,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||||
`,
|
`,
|
||||||
icon: FiX,
|
icon: X,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "edit_booking",
|
id: "edit_booking",
|
||||||
@@ -157,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
actions: [
|
actions: [
|
||||||
{
|
{
|
||||||
id: "reschedule",
|
id: "reschedule",
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
label: t("reschedule_booking"),
|
label: t("reschedule_booking"),
|
||||||
href: `/reschedule/${booking.uid}${
|
href: `/reschedule/${booking.uid}${
|
||||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||||
@@ -165,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "reschedule_request",
|
id: "reschedule_request",
|
||||||
icon: FiSend,
|
icon: Send,
|
||||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||||
label: t("send_reschedule_request"),
|
label: t("send_reschedule_request"),
|
||||||
onClick: () => {
|
onClick: () => {
|
||||||
@@ -178,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
setIsOpenLocationDialog(true);
|
setIsOpenLocationDialog(true);
|
||||||
},
|
},
|
||||||
icon: FiMapPin,
|
icon: MapPin,
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const chargeCardActions: ActionType[] = [
|
||||||
|
{
|
||||||
|
id: "charge_card",
|
||||||
|
label: cardCharged ? t("no_show_fee_charged") : t("collect_no_show_fee"),
|
||||||
|
disabled: cardCharged,
|
||||||
|
onClick: () => {
|
||||||
|
setChargeCardDialogIsOpen(true);
|
||||||
|
},
|
||||||
|
icon: CreditCard,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
if (isTabRecurring && isRecurring) {
|
if (isTabRecurring && isRecurring) {
|
||||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||||
}
|
}
|
||||||
@@ -194,7 +213,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
|
|
||||||
const RequestSentMessage = () => {
|
const RequestSentMessage = () => {
|
||||||
return (
|
return (
|
||||||
<Badge startIcon={FiSend} size="md" variant="gray" data-testid="request_reschedule_sent">
|
<Badge startIcon={Send} size="md" variant="gray" data-testid="request_reschedule_sent">
|
||||||
{t("reschedule_request_sent")}
|
{t("reschedule_request_sent")}
|
||||||
</Badge>
|
</Badge>
|
||||||
);
|
);
|
||||||
@@ -237,10 +256,14 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const title = booking.title;
|
||||||
|
// To be used after we run query on legacy bookings
|
||||||
|
// const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||||
|
|
||||||
const showRecordingsButtons =
|
const showRecordingsButtons =
|
||||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||||
|
|
||||||
const title = booking.title;
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<RescheduleDialog
|
<RescheduleDialog
|
||||||
@@ -254,6 +277,15 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
isOpenDialog={isOpenSetLocationDialog}
|
isOpenDialog={isOpenSetLocationDialog}
|
||||||
setShowLocationModal={setIsOpenLocationDialog}
|
setShowLocationModal={setIsOpenLocationDialog}
|
||||||
/>
|
/>
|
||||||
|
{booking.paid && (
|
||||||
|
<ChargeCardDialog
|
||||||
|
isOpenDialog={chargeCardDialogIsOpen}
|
||||||
|
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||||
|
bookingId={booking.id}
|
||||||
|
paymentAmount={booking?.payment[0].amount}
|
||||||
|
paymentCurrency={booking?.payment[0].currency}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{showRecordingsButtons && (
|
{showRecordingsButtons && (
|
||||||
<ViewRecordingsDialog
|
<ViewRecordingsDialog
|
||||||
booking={booking}
|
booking={booking}
|
||||||
@@ -321,7 +353,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
)}
|
)}
|
||||||
{booking.paid && (
|
{booking.paid && (
|
||||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||||
{t("paid")}
|
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
{recurringDates !== undefined && (
|
{recurringDates !== undefined && (
|
||||||
@@ -423,6 +455,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<RequestSentMessage />
|
<RequestSentMessage />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
|
||||||
|
<div className="ml-2">
|
||||||
|
<TableActions actions={chargeCardActions} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
</>
|
</>
|
||||||
@@ -476,7 +513,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
|||||||
);
|
);
|
||||||
})}>
|
})}>
|
||||||
<div className="text-default">
|
<div className="text-default">
|
||||||
<FiRefreshCcw
|
<RefreshCcw
|
||||||
strokeWidth="3"
|
strokeWidth="3"
|
||||||
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -2,11 +2,10 @@ import { useRouter } from "next/router";
|
|||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Button, TextArea } from "@calcom/ui";
|
import { Button, TextArea } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
booking: {
|
booking: {
|
||||||
@@ -34,7 +33,6 @@ export default function CancelBooking(props: Props) {
|
|||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
const [error, setError] = useState<string | null>(booking ? null : t("booking_already_cancelled"));
|
||||||
useTheme(props.theme);
|
|
||||||
|
|
||||||
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
const cancelBookingRef = useCallback((node: HTMLTextAreaElement) => {
|
||||||
if (node !== null) {
|
if (node !== null) {
|
||||||
@@ -48,7 +46,7 @@ export default function CancelBooking(props: Props) {
|
|||||||
{error && (
|
{error && (
|
||||||
<div className="mt-8">
|
<div className="mt-8">
|
||||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<FiX className="h-6 w-6 text-red-600" />
|
<X className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
|
|||||||
@@ -194,6 +194,7 @@ export const SlotPicker = ({
|
|||||||
bookingAttendees={bookingAttendees}
|
bookingAttendees={bookingAttendees}
|
||||||
recurringCount={recurringEventCount}
|
recurringCount={recurringEventCount}
|
||||||
ethSignature={ethSignature}
|
ethSignature={ethSignature}
|
||||||
|
duration={parseInt(duration)}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import { FiGlobe } from "@calcom/ui/components/icon";
|
import { Globe } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -18,7 +18,7 @@ export function TimezoneDropdown({
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
<div className="dark:focus-within:bg-darkgray-200 dark:bg-darkgray-100 dark:hover:bg-darkgray-200 !mt-3 flex w-full max-w-[20rem] items-center rounded-[4px] px-1 text-sm font-medium focus-within:bg-gray-200 hover:bg-gray-100 lg:max-w-[12rem] [&_p]:focus-within:text-gray-900 dark:[&_p]:focus-within:text-white [&_svg]:focus-within:text-gray-900 dark:[&_svg]:focus-within:text-white">
|
||||||
<FiGlobe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
<Globe className="dark:text-darkgray-600 flex h-4 w-4 text-gray-600 ltr:mr-2 rtl:ml-2" />
|
||||||
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
<TimeOptions onSelectTimeZone={handleSelectTimeZone} />
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -21,11 +21,10 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
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 { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { HeadSeo, useCalcomTheme } from "@calcom/ui";
|
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||||
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
|
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||||
|
|
||||||
@@ -105,8 +104,9 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
|
|
||||||
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
const [recurringEventCount, setRecurringEventCount] = useState(eventType.recurringEvent?.count);
|
||||||
|
|
||||||
|
/*
|
||||||
const telemetry = useTelemetry();
|
const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
@@ -114,7 +114,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/availability", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]); */
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
// get dynamic user list here
|
// get dynamic user list here
|
||||||
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
const userList = eventType.users ? eventType.users.map((user) => user.username).filter(notEmpty) : [];
|
||||||
@@ -124,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
[timeZone]
|
[timeZone]
|
||||||
);
|
);
|
||||||
const paymentAppData = getPaymentAppData(eventType);
|
const paymentAppData = getPaymentAppData(eventType);
|
||||||
|
const paymentAmount = () => {
|
||||||
|
return;
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>;
|
||||||
|
};
|
||||||
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.
|
||||||
@@ -181,7 +191,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
style={availabilityDatePickerEmbedStyles}
|
style={availabilityDatePickerEmbedStyles}
|
||||||
className={classNames(
|
className={classNames(
|
||||||
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted pb-4 md:pb-0",
|
||||||
"border-subtle md:rounded-md md:border",
|
"border-booker md:border-booker-width md:rounded-md",
|
||||||
isEmbed && "mx-auto"
|
isEmbed && "mx-auto"
|
||||||
)}>
|
)}>
|
||||||
<div className="md:flex">
|
<div className="md:flex">
|
||||||
@@ -200,7 +210,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
" text-default",
|
" text-default",
|
||||||
"ltr:mr-[10px] rtl:ml-[10px]"
|
"ltr:mr-[10px] rtl:ml-[10px]"
|
||||||
)}>
|
)}>
|
||||||
<FiUser
|
<User
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||||
"mt-[2px]"
|
"mt-[2px]"
|
||||||
@@ -211,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{!rescheduleUid && eventType.recurringEvent && (
|
{!rescheduleUid && eventType.recurringEvent && (
|
||||||
<div className="flex items-start text-sm font-medium">
|
<div className="flex items-start text-sm font-medium">
|
||||||
<FiRefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
<RefreshCcw className="float-left mt-[7px] ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] " />
|
||||||
<div>
|
<div>
|
||||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||||
</p>
|
</p>
|
||||||
<input
|
|
||||||
type="number"
|
<NumberInput
|
||||||
|
defaultValue={eventType.recurringEvent.count}
|
||||||
min="1"
|
min="1"
|
||||||
max={eventType.recurringEvent.count}
|
max={eventType.recurringEvent.count}
|
||||||
className="w-15 border-default bg-default dark:border-empthasis h-7 rounded-sm text-sm font-medium [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
isFullWidth={false}
|
||||||
defaultValue={eventType.recurringEvent.count}
|
className="me-2 inline w-16"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
setRecurringEventCount(parseInt(event?.target.value));
|
setRecurringEventCount(parseInt(event?.target.value));
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<p className="inline">
|
<p className="inline">
|
||||||
{t("occurrence", {
|
{t("occurrence", {
|
||||||
count: recurringEventCount,
|
count: recurringEventCount,
|
||||||
@@ -236,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
|||||||
)}
|
)}
|
||||||
{paymentAppData.price > 0 && (
|
{paymentAppData.price > 0 && (
|
||||||
<p className="-ml-2 px-2 text-sm font-medium">
|
<p className="-ml-2 px-2 text-sm font-medium">
|
||||||
<FiCreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<IntlProvider locale="en">
|
{paymentAppData.paymentOption === "HOLD" ? (
|
||||||
<FormattedNumber
|
<>
|
||||||
value={paymentAppData.price / 100.0}
|
{t("no_show_fee_amount", {
|
||||||
style="currency"
|
amount: paymentAppData.price / 100.0,
|
||||||
currency={paymentAppData.currency?.toUpperCase()}
|
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||||
/>
|
})}
|
||||||
</IntlProvider>
|
</>
|
||||||
|
) : (
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={paymentAppData.price / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={paymentAppData.currency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
)}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
{timezoneDropdown}
|
{timezoneDropdown}
|
||||||
|
|||||||
@@ -32,17 +32,18 @@ import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocati
|
|||||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME, MINUTES_TO_BOOK } from "@calcom/lib/constants";
|
||||||
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
import useGetBrandingColours from "@calcom/lib/getBrandColours";
|
||||||
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";
|
||||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||||
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
import { Button, Form, Tooltip, useCalcomTheme } from "@calcom/ui";
|
||||||
import { FiAlertTriangle, FiCalendar, FiRefreshCw, FiUser } from "@calcom/ui/components/icon";
|
import { AlertTriangle, Calendar, RefreshCw, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
@@ -210,8 +211,11 @@ const BookingPage = ({
|
|||||||
hashedLink,
|
hashedLink,
|
||||||
...restProps
|
...restProps
|
||||||
}: BookingPageProps) => {
|
}: BookingPageProps) => {
|
||||||
|
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
|
||||||
|
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { duration: queryDuration } = useRouterQuery("duration");
|
const { duration: queryDuration } = useRouterQuery("duration");
|
||||||
|
const { date: queryDate } = useRouterQuery("date");
|
||||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
const shouldAlignCentrallyInEmbed = useEmbedNonStylesConfig("align") !== "left";
|
||||||
@@ -227,6 +231,15 @@ const BookingPage = ({
|
|||||||
}),
|
}),
|
||||||
{}
|
{}
|
||||||
);
|
);
|
||||||
|
const reserveSlot = () => {
|
||||||
|
if (queryDuration) {
|
||||||
|
reserveSlotMutation.mutate({
|
||||||
|
eventTypeId: eventType.id,
|
||||||
|
slotUtcStartDate: dayjs(queryDate).utc().format(),
|
||||||
|
slotUtcEndDate: dayjs(queryDate).utc().add(parseInt(queryDuration), "minutes").format(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
// Define duration now that we support multiple duration eventTypes
|
// Define duration now that we support multiple duration eventTypes
|
||||||
let duration = eventType.length;
|
let duration = eventType.length;
|
||||||
if (
|
if (
|
||||||
@@ -239,13 +252,19 @@ const BookingPage = ({
|
|||||||
}
|
}
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (top !== window) {
|
/* if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(
|
telemetry.event(
|
||||||
telemetryEventTypes.embedView,
|
telemetryEventTypes.embedView,
|
||||||
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
collectPageParameters("/book", { isTeamBooking: document.URL.includes("team/") })
|
||||||
);
|
);
|
||||||
}
|
} */
|
||||||
|
reserveSlot();
|
||||||
|
const interval = setInterval(reserveSlot, parseInt(MINUTES_TO_BOOK) * 60 * 1000 - 2000);
|
||||||
|
return () => {
|
||||||
|
clearInterval(interval);
|
||||||
|
removeSelectedSlotMarkMutation.mutate();
|
||||||
|
};
|
||||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
@@ -528,17 +547,17 @@ const BookingPage = ({
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main",
|
"main",
|
||||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-default dark:border",
|
isBackgroundTransparent ? "" : "bg-default dark:bg-muted",
|
||||||
"dark:border-darkgray-300 rounded-md sm:border"
|
"border-booker sm:border-booker-width rounded-md"
|
||||||
)}>
|
)}>
|
||||||
<div className="sm:flex">
|
<div className="sm:flex">
|
||||||
{showEventTypeDetails && (
|
{showEventTypeDetails && (
|
||||||
<div className="sm:dark:border-darkgray-300 text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
<div className="sm:border-subtle text-default flex flex-col px-6 pt-6 pb-0 sm:w-1/2 sm:border-r sm:pb-6">
|
||||||
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
<BookingDescription isBookingPage profile={profile} eventType={eventType}>
|
||||||
<BookingDescriptionPayment eventType={eventType} />
|
<BookingDescriptionPayment eventType={eventType} t={t} />
|
||||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||||
<div className="dark:text-inverted text-default items-start text-sm font-medium">
|
<div className="dark:text-inverted text-default items-start text-sm font-medium">
|
||||||
<FiRefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<RefreshCw className="ml-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<p className="-ml-2 inline-block items-center px-2">
|
<p className="-ml-2 inline-block items-center px-2">
|
||||||
{getEveryFreqFor({
|
{getEveryFreqFor({
|
||||||
t,
|
t,
|
||||||
@@ -549,7 +568,7 @@ const BookingPage = ({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiCalendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
<div className="text-sm font-medium">
|
<div className="text-sm font-medium">
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||||
@@ -580,7 +599,7 @@ const BookingPage = ({
|
|||||||
{t("former_time")}
|
{t("former_time")}
|
||||||
</p>
|
</p>
|
||||||
<p className="line-through ">
|
<p className="line-through ">
|
||||||
<FiCalendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
<Calendar className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||||
{isClientTimezoneAvailable &&
|
{isClientTimezoneAvailable &&
|
||||||
typeof booking.startTime === "string" &&
|
typeof booking.startTime === "string" &&
|
||||||
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
||||||
@@ -589,7 +608,7 @@ const BookingPage = ({
|
|||||||
)}
|
)}
|
||||||
{!!eventType.seatsPerTimeSlot && (
|
{!!eventType.seatsPerTimeSlot && (
|
||||||
<div className="text-bookinghighlight flex items-start text-sm">
|
<div className="text-bookinghighlight flex items-start text-sm">
|
||||||
<FiUser
|
<User
|
||||||
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
className={`ml-[2px] mt-[2px] inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px] ${
|
||||||
currentSlotBooking &&
|
currentSlotBooking &&
|
||||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||||
@@ -649,9 +668,9 @@ const BookingPage = ({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
</Form>
|
</Form>
|
||||||
{(mutation.isError || recurringMutation.isError) && (
|
{mutation.isError || recurringMutation.isError ? (
|
||||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||||
)}
|
) : null}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -672,7 +691,7 @@ function ErrorMessage({ error }: { error: unknown }) {
|
|||||||
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
<div data-testid="booking-fail" className="mt-2 border-l-4 border-blue-400 bg-blue-50 p-4">
|
||||||
<div className="flex">
|
<div className="flex">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<FiAlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
<AlertTriangle className="h-5 w-5 text-blue-400" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
<div className="ms-3">
|
<div className="ms-3">
|
||||||
<p className="text-sm text-blue-700">
|
<p className="text-sm text-blue-700">
|
||||||
|
|||||||
@@ -0,0 +1,100 @@
|
|||||||
|
import { Trans } from "next-i18next";
|
||||||
|
import { useState } from "react";
|
||||||
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
import { IntlProvider, FormattedNumber } from "react-intl";
|
||||||
|
|
||||||
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
import {
|
||||||
|
Button,
|
||||||
|
Dialog,
|
||||||
|
DialogClose,
|
||||||
|
DialogContent,
|
||||||
|
DialogFooter,
|
||||||
|
DialogHeader,
|
||||||
|
showToast,
|
||||||
|
} from "@calcom/ui";
|
||||||
|
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
interface IRescheduleDialog {
|
||||||
|
isOpenDialog: boolean;
|
||||||
|
setIsOpenDialog: Dispatch<SetStateAction<boolean>>;
|
||||||
|
bookingId: number;
|
||||||
|
paymentAmount: number;
|
||||||
|
paymentCurrency: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
const utils = trpc.useContext();
|
||||||
|
const { isOpenDialog, setIsOpenDialog, bookingId } = props;
|
||||||
|
const [chargeError, setChargeError] = useState(false);
|
||||||
|
const chargeCardMutation = trpc.viewer.payments.chargeCard.useMutation({
|
||||||
|
onSuccess: () => {
|
||||||
|
utils.viewer.bookings.invalidate();
|
||||||
|
setIsOpenDialog(false);
|
||||||
|
showToast("Charge successful", "success");
|
||||||
|
},
|
||||||
|
onError: () => {
|
||||||
|
setChargeError(true);
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
|
<DialogContent>
|
||||||
|
<div className="flex flex-row space-x-3">
|
||||||
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
|
<CreditCard className="m-auto h-6 w-6" />
|
||||||
|
</div>
|
||||||
|
<div className="pt-1">
|
||||||
|
<DialogHeader title={t("charge_card")} />
|
||||||
|
<Trans i18nKey="charge_card_dialog_body">
|
||||||
|
<p className="text-sm text-gray-500">
|
||||||
|
You are about to charge the attendee{" "}
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={props.paymentAmount / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={props.paymentCurrency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
. Are you sure you want to continue?
|
||||||
|
</p>
|
||||||
|
</Trans>
|
||||||
|
|
||||||
|
{chargeError && (
|
||||||
|
<div className="mt-4 flex text-red-500">
|
||||||
|
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||||
|
<p className="text-sm">{t("error_charging_card")}</p>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<DialogFooter>
|
||||||
|
<DialogClose />
|
||||||
|
<Button
|
||||||
|
data-testid="send_request"
|
||||||
|
disabled={chargeCardMutation.isLoading || chargeError}
|
||||||
|
onClick={() =>
|
||||||
|
chargeCardMutation.mutate({
|
||||||
|
bookingId,
|
||||||
|
})
|
||||||
|
}>
|
||||||
|
<Trans i18nKey="charge_card_confirm">
|
||||||
|
Charge attendee{" "}
|
||||||
|
<IntlProvider locale="en">
|
||||||
|
<FormattedNumber
|
||||||
|
value={props.paymentAmount / 100.0}
|
||||||
|
style="currency"
|
||||||
|
currency={props.paymentCurrency?.toUpperCase()}
|
||||||
|
/>
|
||||||
|
</IntlProvider>
|
||||||
|
</Trans>
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -19,7 +19,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Input } from "@calcom/ui";
|
import { Input } from "@calcom/ui";
|
||||||
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
import { Button, Dialog, DialogContent, DialogFooter, Form, PhoneInput } from "@calcom/ui";
|
||||||
import { FiMapPin } from "@calcom/ui/components/icon";
|
import { MapPin } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
@@ -218,7 +218,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="bg-subtle mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-subtle mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<FiMapPin className="text-emphasis h-6 w-6" />
|
<MapPin className="text-emphasis h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
TextArea,
|
TextArea,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiClock } from "@calcom/ui/components/icon";
|
import { Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface IRescheduleDialog {
|
interface IRescheduleDialog {
|
||||||
isOpenDialog: boolean;
|
isOpenDialog: boolean;
|
||||||
@@ -44,7 +44,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogContent>
|
<DialogContent>
|
||||||
<div className="flex flex-row space-x-3">
|
<div className="flex flex-row space-x-3">
|
||||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||||
<FiClock className="m-auto h-6 w-6" />
|
<Clock className="m-auto h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="pt-1">
|
<div className="pt-1">
|
||||||
<DialogHeader title={t("send_reschedule_request")} />
|
<DialogHeader title={t("send_reschedule_request")} />
|
||||||
|
|||||||
@@ -10,14 +10,15 @@ import type { EventNameObjectType } from "@calcom/core/event";
|
|||||||
import { getEventName } from "@calcom/core/event";
|
import { getEventName } from "@calcom/core/event";
|
||||||
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||||
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
import DestinationCalendarSelector from "@calcom/features/calendars/DestinationCalendarSelector";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
import { FormBuilder } from "@calcom/features/form-builder/FormBuilder";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
@@ -58,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
||||||
@@ -79,11 +81,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
const eventNamePlaceholder = getEventName({
|
const eventNamePlaceholder = getEventName({
|
||||||
...eventNameObject,
|
...eventNameObject,
|
||||||
eventName: formMethods.watch("eventName"),
|
eventName: formMethods.watch("eventName"),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||||
|
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||||
@@ -127,19 +137,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<TextField
|
<TextField
|
||||||
label={t("event_name_in_calendar")}
|
label={t("event_name_in_calendar")}
|
||||||
type="text"
|
type="text"
|
||||||
|
{...shouldLockDisableProps("eventName")}
|
||||||
placeholder={eventNamePlaceholder}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
defaultValue={eventType.eventName || ""}
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
|
||||||
StartIcon={FiEdit}
|
|
||||||
variant="icon"
|
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 hover:bg-transparent"
|
size="sm"
|
||||||
onClick={() => setShowEventNameTip((old) => !old)}
|
|
||||||
aria-label="edit custom name"
|
aria-label="edit custom name"
|
||||||
/>
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
onClick={() => setShowEventNameTip((old) => !old)}>
|
||||||
|
<Edit className="h-4 w-4" />
|
||||||
|
</Button>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -149,6 +159,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
description={t("booking_questions_description")}
|
description={t("booking_questions_description")}
|
||||||
addFieldLabel={t("add_a_booking_question")}
|
addFieldLabel={t("add_a_booking_question")}
|
||||||
formProp="bookingFields"
|
formProp="bookingFields"
|
||||||
|
{...shouldLockDisableProps("bookingFields")}
|
||||||
dataStore={{
|
dataStore={{
|
||||||
options: {
|
options: {
|
||||||
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
||||||
@@ -157,6 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
/>
|
/>
|
||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
|
eventType={eventType}
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
metadata={eventType.metadata}
|
metadata={eventType.metadata}
|
||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
@@ -170,6 +182,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("disable_notes")}
|
title={t("disable_notes")}
|
||||||
|
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||||
description={t("disable_notes_description")}
|
description={t("disable_notes_description")}
|
||||||
checked={value}
|
checked={value}
|
||||||
onCheckedChange={(e) => onChange(e)}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
@@ -184,6 +197,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("redirect_success_booking")}
|
title={t("redirect_success_booking")}
|
||||||
|
{...successRedirectUrlLocked}
|
||||||
description={t("redirect_url_description")}
|
description={t("redirect_url_description")}
|
||||||
checked={redirectUrlVisible}
|
checked={redirectUrlVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
@@ -196,6 +210,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
className="w-full"
|
className="w-full"
|
||||||
label={t("redirect_success_booking")}
|
label={t("redirect_success_booking")}
|
||||||
labelSrOnly
|
labelSrOnly
|
||||||
|
disabled={successRedirectUrlLocked.disabled}
|
||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
required={redirectUrlVisible}
|
required={redirectUrlVisible}
|
||||||
type="text"
|
type="text"
|
||||||
@@ -218,6 +233,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
data-testid="hashedLinkCheck"
|
data-testid="hashedLinkCheck"
|
||||||
title={t("private_link")}
|
title={t("private_link")}
|
||||||
|
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||||
description={t("private_link_description", { appName: APP_NAME })}
|
description={t("private_link_description", { appName: APP_NAME })}
|
||||||
checked={hashedLinkVisible}
|
checked={hashedLinkVisible}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
@@ -239,6 +255,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
|
size="sm"
|
||||||
|
type="button"
|
||||||
|
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 !py-0 hover:bg-transparent"
|
||||||
|
aria-label="copy link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
if (eventType.hashedLink) {
|
if (eventType.hashedLink) {
|
||||||
@@ -246,10 +266,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
} else {
|
} else {
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
}
|
}
|
||||||
}}
|
}}>
|
||||||
className="hover:stroke-3 hover:text-emphasis hover:bg-transparent"
|
<Copy className="h-4 w-4" />
|
||||||
type="button">
|
|
||||||
<FiCopy />
|
|
||||||
</Button>
|
</Button>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
}
|
}
|
||||||
@@ -262,54 +280,62 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<>
|
||||||
data-testid="offer-seats-toggle"
|
<SettingsToggle
|
||||||
title={t("offer_seats")}
|
data-testid="offer-seats-toggle"
|
||||||
description={t("offer_seats_description")}
|
title={t("offer_seats")}
|
||||||
checked={value}
|
{...seatsLocked}
|
||||||
onCheckedChange={(e) => {
|
description={t("offer_seats_description")}
|
||||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
checked={value}
|
||||||
if (e) {
|
disabled={noShowFeeEnabled}
|
||||||
toggleGuests(false);
|
onCheckedChange={(e) => {
|
||||||
formMethods.setValue("requiresConfirmation", false);
|
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||||
setRequiresConfirmation(false);
|
if (e) {
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
toggleGuests(false);
|
||||||
} else {
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
setRequiresConfirmation(false);
|
||||||
toggleGuests(true);
|
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||||
}
|
} else {
|
||||||
onChange(e);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
}}>
|
toggleGuests(true);
|
||||||
<Controller
|
}
|
||||||
name="seatsPerTimeSlot"
|
onChange(e);
|
||||||
control={formMethods.control}
|
}}>
|
||||||
defaultValue={eventType.seatsPerTimeSlot}
|
<Controller
|
||||||
render={({ field: { value, onChange } }) => (
|
name="seatsPerTimeSlot"
|
||||||
<div className="lg:-ml-2">
|
control={formMethods.control}
|
||||||
<TextField
|
defaultValue={eventType.seatsPerTimeSlot}
|
||||||
required
|
render={({ field: { value, onChange } }) => (
|
||||||
name="seatsPerTimeSlot"
|
<div className="lg:-ml-2">
|
||||||
labelSrOnly
|
<TextField
|
||||||
label={t("number_of_seats")}
|
required
|
||||||
type="number"
|
name="seatsPerTimeSlot"
|
||||||
defaultValue={value || 2}
|
labelSrOnly
|
||||||
min={1}
|
label={t("number_of_seats")}
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
type="number"
|
||||||
onChange={(e) => {
|
disabled={seatsLocked.disabled}
|
||||||
onChange(Math.abs(Number(e.target.value)));
|
defaultValue={value || 2}
|
||||||
}}
|
min={1}
|
||||||
/>
|
className="w-24"
|
||||||
<div className="mt-2">
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
<Checkbox
|
onChange={(e) => {
|
||||||
description={t("show_attendees")}
|
onChange(Math.abs(Number(e.target.value)));
|
||||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
}}
|
||||||
defaultChecked={!!eventType.seatsShowAttendees}
|
|
||||||
/>
|
/>
|
||||||
|
<div className="mt-2">
|
||||||
|
<Checkbox
|
||||||
|
description={t("show_attendees")}
|
||||||
|
disabled={seatsLocked.disabled}
|
||||||
|
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||||
|
defaultChecked={!!eventType.seatsShowAttendees}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
)}
|
/>
|
||||||
/>
|
</SettingsToggle>
|
||||||
</SettingsToggle>
|
{noShowFeeEnabled && <Alert severity="warning" title={t("seats_and_no_show_fee_error")} />}
|
||||||
|
</>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,11 @@ import type { GetAppData, SetAppData } from "@calcom/app-store/EventTypeAppConte
|
|||||||
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
import { EventTypeAppCard } from "@calcom/app-store/_components/EventTypeAppCardInterface";
|
||||||
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
import type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||||
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
import type { EventTypeAppsList } from "@calcom/app-store/utils";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
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 { Button, EmptyScreen } from "@calcom/ui";
|
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||||
import { FiGrid } from "@calcom/ui/components/icon";
|
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
||||||
EventTypeAppCardComponentProps["eventType"];
|
EventTypeAppCardComponentProps["eventType"];
|
||||||
@@ -55,19 +56,39 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
<div className="before:border-0">
|
<div className="before:border-0">
|
||||||
|
{!installedApps?.length && isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_apps_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{!isLoading && !installedApps?.length ? (
|
{!isLoading && !installedApps?.length ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiGrid}
|
Icon={Grid}
|
||||||
headline={t("empty_installed_apps_headline")}
|
headline={t("empty_installed_apps_headline")}
|
||||||
description={t("empty_installed_apps_description")}
|
description={t("empty_installed_apps_description")}
|
||||||
buttonRaw={
|
buttonRaw={
|
||||||
<Button target="_blank" color="secondary" href="/apps">
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
{t("empty_installed_apps_button")}{" "}
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
</Button>
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<Button target="_blank" color="secondary" href="/apps">
|
||||||
|
{t("empty_installed_apps_button")}{" "}
|
||||||
|
</Button>
|
||||||
|
)
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
))}
|
))}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div>
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
<div>
|
||||||
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">Available Apps</h2>
|
{!isLoading && notInstalledApps?.length ? (
|
||||||
) : null}
|
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||||
<div className="before:border-0">
|
) : null}
|
||||||
{notInstalledApps?.map((app) => (
|
<div className="before:border-0">
|
||||||
<EventTypeAppCard
|
{notInstalledApps?.map((app) => (
|
||||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
<EventTypeAppCard
|
||||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||||
key={app.slug}
|
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||||
app={app}
|
key={app.slug}
|
||||||
eventType={eventType}
|
app={app}
|
||||||
/>
|
eventType={eventType}
|
||||||
))}
|
/>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
+122
-49
@@ -1,29 +1,29 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
import { SchedulingType } from "@prisma/client";
|
||||||
|
import type { FormValues, EventTypeSetup } from "pages/event-types/[type]";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import type { OptionProps, SingleValueProps } from "react-select";
|
import type { OptionProps, SingleValueProps } from "react-select";
|
||||||
import { components } from "react-select";
|
import { components } from "react-select";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { weekdayNames } from "@calcom/lib/weekday";
|
import { weekdayNames } from "@calcom/lib/weekday";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||||
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
|
||||||
|
|
||||||
type AvailabilityOption = {
|
type AvailabilityOption = {
|
||||||
label: string;
|
label: string;
|
||||||
value: number;
|
value: number;
|
||||||
isDefault: boolean;
|
isDefault: boolean;
|
||||||
|
isManaged?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.Option {...props}>
|
<components.Option {...props}>
|
||||||
@@ -33,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
|||||||
{t("default")}
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</components.Option>
|
</components.Option>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||||
const { label, isDefault } = props.data;
|
const { label, isDefault, isManaged = false } = props.data;
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
<components.SingleValue {...props}>
|
<components.SingleValue {...props}>
|
||||||
@@ -48,50 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
|||||||
{t("default")}
|
{t("default")}
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
{isManaged && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("managed")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
</components.SingleValue>
|
</components.SingleValue>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const AvailabilitySelect = ({
|
const AvailabilitySelect = ({
|
||||||
className = "",
|
className = "",
|
||||||
isLoading,
|
options,
|
||||||
schedules,
|
value,
|
||||||
...props
|
...props
|
||||||
}: {
|
}: {
|
||||||
className?: string;
|
className?: string;
|
||||||
name: string;
|
name: string;
|
||||||
value: number;
|
value: AvailabilityOption | undefined;
|
||||||
isLoading: boolean;
|
options: AvailabilityOption[];
|
||||||
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
isDisabled?: boolean;
|
||||||
onBlur: () => void;
|
onBlur: () => void;
|
||||||
onChange: (value: AvailabilityOption | null) => void;
|
onChange: (value: AvailabilityOption | null) => void;
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
|
||||||
return <SelectSkeletonLoader />;
|
|
||||||
}
|
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
|
||||||
value: schedule.id,
|
|
||||||
label: schedule.name,
|
|
||||||
isDefault: schedule.isDefault,
|
|
||||||
}));
|
|
||||||
|
|
||||||
const value = options.find((option) =>
|
|
||||||
props.value
|
|
||||||
? option.value === props.value
|
|
||||||
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={options}
|
options={options}
|
||||||
|
isDisabled={props.isDisabled}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
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}
|
defaultValue={value}
|
||||||
components={{ Option, SingleValue }}
|
components={{ Option, SingleValue }}
|
||||||
isMulti={false}
|
isMulti={false}
|
||||||
/>
|
/>
|
||||||
@@ -103,17 +97,25 @@ const format = (date: Date, hour12: boolean) =>
|
|||||||
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
new Date(dayjs.utc(date).format("YYYY-MM-DDTHH:mm:ss"))
|
||||||
);
|
);
|
||||||
|
|
||||||
const EventTypeScheduleDetails = () => {
|
const EventTypeScheduleDetails = ({
|
||||||
|
isManagedEventType,
|
||||||
|
selectedScheduleValue,
|
||||||
|
}: {
|
||||||
|
isManagedEventType: boolean;
|
||||||
|
selectedScheduleValue: AvailabilityOption | undefined;
|
||||||
|
}) => {
|
||||||
const { data: loggedInUser } = useMeQuery();
|
const { data: loggedInUser } = useMeQuery();
|
||||||
const timeFormat = loggedInUser?.timeFormat;
|
const timeFormat = loggedInUser?.timeFormat;
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
|
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
{
|
||||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
scheduleId: scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
||||||
|
isManagedEventType,
|
||||||
|
},
|
||||||
|
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId || !!selectedScheduleValue }
|
||||||
);
|
);
|
||||||
|
|
||||||
const filterDays = (dayNum: number) =>
|
const filterDays = (dayNum: number) =>
|
||||||
@@ -157,15 +159,15 @@ const EventTypeScheduleDetails = () => {
|
|||||||
<hr className="border-subtle" />
|
<hr className="border-subtle" />
|
||||||
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
<div className="flex flex-col justify-center gap-2 sm:flex-row sm:justify-between">
|
||||||
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
|
<span className="text-default flex items-center justify-center text-sm sm:justify-start">
|
||||||
<FiGlobe className="ltr:mr-2 rtl:ml-2" />
|
<Globe className="h-3.5 w-3.5 ltr:mr-2 rtl:ml-2" />
|
||||||
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
{schedule?.timeZone || <SkeletonText className="block h-5 w-32" />}
|
||||||
</span>
|
</span>
|
||||||
{!!schedule?.id && (
|
{!!schedule?.id && !schedule.isManaged && (
|
||||||
<Button
|
<Button
|
||||||
href={`/availability/${schedule.id}`}
|
href={`/availability/${schedule.id}`}
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
EndIcon={FiExternalLink}
|
EndIcon={ExternalLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer">
|
rel="noopener noreferrer">
|
||||||
{t("edit_availability")}
|
{t("edit_availability")}
|
||||||
@@ -176,14 +178,23 @@ const EventTypeScheduleDetails = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const EventTypeSchedule = () => {
|
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
|
const { shouldLockIndicator, shouldLockDisableProps, isManagedEventType, isChildrenManagedEventType } =
|
||||||
|
useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
const watchSchedule = watch("schedule");
|
||||||
|
|
||||||
if (!schedules?.schedules.length && !isLoading)
|
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||||
|
|
||||||
|
if (!data?.schedules.length && !isLoading)
|
||||||
return (
|
return (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiClock}
|
Icon={Clock}
|
||||||
headline={t("new_schedule_heading")}
|
headline={t("new_schedule_heading")}
|
||||||
description={t("new_schedule_description")}
|
description={t("new_schedule_description")}
|
||||||
buttonRaw={<NewScheduleButton fromEventType />}
|
buttonRaw={<NewScheduleButton fromEventType />}
|
||||||
@@ -191,22 +202,65 @@ const EventTypeSchedule = () => {
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const schedules = data?.schedules || [];
|
||||||
|
|
||||||
|
const options = schedules.map((schedule) => ({
|
||||||
|
value: schedule.id,
|
||||||
|
label: schedule.name,
|
||||||
|
isDefault: schedule.isDefault,
|
||||||
|
isManaged: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
// We are showing a managed event for a team admin, so adding the option to let members choose their schedule
|
||||||
|
if (isManagedEventType) {
|
||||||
|
options.push({
|
||||||
|
value: 0,
|
||||||
|
label: t("members_default_schedule"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// We are showing a managed event for a member and team owner selected their own schedule, so adding
|
||||||
|
// the managed schedule option
|
||||||
|
if (
|
||||||
|
isChildrenManagedEventType &&
|
||||||
|
watchSchedule &&
|
||||||
|
!schedules.find((schedule) => schedule.id === watchSchedule)
|
||||||
|
) {
|
||||||
|
options.push({
|
||||||
|
value: watchSchedule,
|
||||||
|
label: eventType.scheduleName ?? t("default_schedule_name"),
|
||||||
|
isDefault: false,
|
||||||
|
isManaged: false,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const value = options.find((option) =>
|
||||||
|
watchSchedule
|
||||||
|
? option.value === watchSchedule
|
||||||
|
: isManagedEventType
|
||||||
|
? option.value === 0
|
||||||
|
: option.value === schedules.find((schedule) => schedule.isDefault)?.id
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
<div>
|
<div>
|
||||||
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||||
{t("availability")}
|
{t("availability")}
|
||||||
|
{shouldLockIndicator("availability")}
|
||||||
</label>
|
</label>
|
||||||
<Controller
|
<Controller
|
||||||
name="schedule"
|
name="schedule"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<>
|
<>
|
||||||
<AvailabilitySelect
|
<AvailabilitySelect
|
||||||
value={field.value}
|
value={value}
|
||||||
|
options={options}
|
||||||
onBlur={field.onBlur}
|
onBlur={field.onBlur}
|
||||||
|
isDisabled={shouldLockDisableProps("schedule").disabled}
|
||||||
name={field.name}
|
name={field.name}
|
||||||
schedules={schedules?.schedules || []}
|
|
||||||
isLoading={isLoading}
|
|
||||||
onChange={(selected) => {
|
onChange={(selected) => {
|
||||||
field.onChange(selected?.value || null);
|
field.onChange(selected?.value || null);
|
||||||
}}
|
}}
|
||||||
@@ -215,12 +269,21 @@ const EventTypeSchedule = () => {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<EventTypeScheduleDetails />
|
{value?.value !== 0 ? (
|
||||||
|
<EventTypeScheduleDetails
|
||||||
|
selectedScheduleValue={value}
|
||||||
|
isManagedEventType={isManagedEventType || isChildrenManagedEventType}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
isManagedEventType && (
|
||||||
|
<p className="!mt-2 ml-1 text-sm text-gray-600">{t("members_default_schedule_description")}</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const UseCommonScheduleSettingsToggle = () => {
|
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { resetField, setValue } = useFormContext<FormValues>();
|
const { resetField, setValue } = useFormContext<FormValues>();
|
||||||
return (
|
return (
|
||||||
@@ -239,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
|
|||||||
}}
|
}}
|
||||||
title={t("choose_common_schedule_team_event")}
|
title={t("choose_common_schedule_team_event")}
|
||||||
description={t("choose_common_schedule_team_event_description")}>
|
description={t("choose_common_schedule_team_event_description")}>
|
||||||
<EventTypeSchedule />
|
<EventTypeSchedule eventType={eventType} />
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
export const EventAvailabilityTab = ({
|
||||||
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
eventType,
|
||||||
|
isTeamEvent,
|
||||||
|
}: {
|
||||||
|
eventType: EventTypeSetup;
|
||||||
|
isTeamEvent: boolean;
|
||||||
|
}) => {
|
||||||
|
return isTeamEvent && eventType.schedulingType !== SchedulingType.MANAGED ? (
|
||||||
|
<UseCommonScheduleSettingsToggle eventType={eventType} />
|
||||||
|
) : (
|
||||||
|
<EventTypeSchedule eventType={eventType} />
|
||||||
|
);
|
||||||
};
|
};
|
||||||
@@ -7,6 +7,7 @@ import type { UseFormRegisterReturn } from "react-hook-form";
|
|||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
||||||
import type { SingleValue } from "react-select";
|
import type { SingleValue } from "react-select";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
import type { DurationType } from "@calcom/lib/convertToNewDurationType";
|
||||||
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
import convertToNewDurationType from "@calcom/lib/convertToNewDurationType";
|
||||||
@@ -14,17 +15,8 @@ import findDurationType from "@calcom/lib/findDurationType";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { PeriodType } from "@calcom/prisma/client";
|
import type { PeriodType } from "@calcom/prisma/client";
|
||||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||||
import {
|
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
Button,
|
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
DateRangePicker,
|
|
||||||
Input,
|
|
||||||
InputField,
|
|
||||||
Label,
|
|
||||||
Select,
|
|
||||||
SettingsToggle,
|
|
||||||
TextField,
|
|
||||||
} from "@calcom/ui";
|
|
||||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
|
||||||
|
|
||||||
const MinimumBookingNoticeInput = React.forwardRef<
|
const MinimumBookingNoticeInput = React.forwardRef<
|
||||||
HTMLInputElement,
|
HTMLInputElement,
|
||||||
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
<div className="w-1/2 md:w-full">
|
<div className="w-1/2 md:w-full">
|
||||||
<InputField
|
<InputField
|
||||||
required
|
required
|
||||||
|
disabled={passThroughProps.disabled}
|
||||||
defaultValue={minimumBookingNoticeDisplayValues.value}
|
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
setMinimumBookingNoticeDisplayValues({
|
setMinimumBookingNoticeDisplayValues({
|
||||||
@@ -95,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
</div>
|
</div>
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={passThroughProps.disabled}
|
||||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||||
defaultValue={durationTypeOptions.find(
|
defaultValue={durationTypeOptions.find(
|
||||||
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||||
@@ -146,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
defaultValue: periodType?.type,
|
defaultValue: periodType?.type,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { shouldLockIndicator, shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const bookingLimitsLocked = shouldLockDisableProps("bookingLimits");
|
||||||
|
const durationLimitsLocked = shouldLockDisableProps("durationLimits");
|
||||||
|
const periodTypeLocked = shouldLockDisableProps("periodType");
|
||||||
|
|
||||||
|
const optionsPeriod = [
|
||||||
|
{ value: 1, label: t("calendar_days") },
|
||||||
|
{ value: 0, label: t("business_days") },
|
||||||
|
];
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<div className="space-y-4 lg:space-y-8">
|
<div className="space-y-4 lg:space-y-8">
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
<Label htmlFor="beforeBufferTime">
|
||||||
|
{t("before_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="beforeBufferTime"
|
name="beforeBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -170,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -183,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
<Label htmlFor="afterBufferTime">
|
||||||
|
{t("after_event")}
|
||||||
|
{shouldLockIndicator("bookingLimits")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="afterBufferTime"
|
name="afterBufferTime"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -202,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
if (val) onChange(val.value);
|
if (val) onChange(val.value);
|
||||||
}}
|
}}
|
||||||
@@ -217,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
<Label htmlFor="minimumBookingNotice">
|
||||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
{t("minimum_booking_notice")}
|
||||||
|
{shouldLockIndicator("minimumBookingNotice")}
|
||||||
|
</Label>
|
||||||
|
<MinimumBookingNoticeInput
|
||||||
|
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||||
|
{...formMethods.register("minimumBookingNotice")}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
<Label htmlFor="slotInterval">
|
||||||
|
{t("slot_interval")}
|
||||||
|
{shouldLockIndicator("slotInterval")}
|
||||||
|
</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="slotInterval"
|
name="slotInterval"
|
||||||
control={formMethods.control}
|
control={formMethods.control}
|
||||||
@@ -239,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
return (
|
return (
|
||||||
<Select
|
<Select
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||||
onChange={(val) => {
|
onChange={(val) => {
|
||||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||||
}}
|
}}
|
||||||
@@ -261,6 +288,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
render={({ field: { value } }) => (
|
render={({ field: { value } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_booking_frequency")}
|
title={t("limit_booking_frequency")}
|
||||||
|
{...bookingLimitsLocked}
|
||||||
description={t("limit_booking_frequency_description")}
|
description={t("limit_booking_frequency_description")}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
@@ -272,7 +300,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
formMethods.setValue("bookingLimits", {});
|
formMethods.setValue("bookingLimits", {});
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
<IntervalLimitsManager
|
||||||
|
disabled={bookingLimitsLocked.disabled}
|
||||||
|
propertyName="bookingLimits"
|
||||||
|
defaultLimit={1}
|
||||||
|
step={1}
|
||||||
|
/>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
@@ -284,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_total_booking_duration")}
|
title={t("limit_total_booking_duration")}
|
||||||
description={t("limit_total_booking_duration_description")}
|
description={t("limit_total_booking_duration_description")}
|
||||||
|
{...durationLimitsLocked}
|
||||||
checked={Object.keys(value ?? {}).length > 0}
|
checked={Object.keys(value ?? {}).length > 0}
|
||||||
onCheckedChange={(active) => {
|
onCheckedChange={(active) => {
|
||||||
if (active) {
|
if (active) {
|
||||||
@@ -297,6 +331,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<IntervalLimitsManager
|
<IntervalLimitsManager
|
||||||
propertyName="durationLimits"
|
propertyName="durationLimits"
|
||||||
defaultLimit={60}
|
defaultLimit={60}
|
||||||
|
disabled={durationLimitsLocked.disabled}
|
||||||
step={15}
|
step={15}
|
||||||
textFieldSuffix={t("minutes")}
|
textFieldSuffix={t("minutes")}
|
||||||
/>
|
/>
|
||||||
@@ -311,13 +346,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("limit_future_bookings")}
|
title={t("limit_future_bookings")}
|
||||||
description={t("limit_future_bookings_description")}
|
description={t("limit_future_bookings_description")}
|
||||||
|
{...periodTypeLocked}
|
||||||
checked={value && value !== "UNLIMITED"}
|
checked={value && value !== "UNLIMITED"}
|
||||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||||
<RadioGroup.Root
|
<RadioGroup.Root
|
||||||
defaultValue={watchPeriodType}
|
defaultValue={watchPeriodType}
|
||||||
value={watchPeriodType}
|
value={watchPeriodType}
|
||||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||||
{PERIOD_TYPES.map((period) => {
|
{PERIOD_TYPES.filter((opt) =>
|
||||||
|
periodTypeLocked.disabled ? watchPeriodType === opt.type : true
|
||||||
|
).map((period) => {
|
||||||
if (period.type === "UNLIMITED") return null;
|
if (period.type === "UNLIMITED") return null;
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
@@ -326,30 +364,42 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||||
)}
|
)}
|
||||||
key={period.type}>
|
key={period.type}>
|
||||||
<RadioGroup.Item
|
{!periodTypeLocked.disabled && (
|
||||||
id={period.type}
|
<RadioGroup.Item
|
||||||
value={period.type}
|
id={period.type}
|
||||||
className="min-w-4 bg-default flex h-4 w-4 cursor-pointer items-center rounded-full border border-black focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
value={period.type}
|
||||||
<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" />
|
className="min-w-4 bg-default border-default flex h-4 w-4 cursor-pointer items-center rounded-full border focus:border-2 focus:outline-none ltr:mr-2 rtl:ml-2">
|
||||||
</RadioGroup.Item>
|
<RadioGroup.Indicator className="after:bg-inverted relative flex h-4 w-4 items-center justify-center after:block after:h-2 after:w-2 after:rounded-full" />
|
||||||
|
</RadioGroup.Item>
|
||||||
|
)}
|
||||||
{period.prefix ? <span>{period.prefix} </span> : null}
|
{period.prefix ? <span>{period.prefix} </span> : null}
|
||||||
{period.type === "ROLLING" && (
|
{period.type === "ROLLING" && (
|
||||||
<div className="flex h-9">
|
<div className="flex items-center">
|
||||||
<Input
|
<TextField
|
||||||
|
labelSrOnly
|
||||||
type="number"
|
type="number"
|
||||||
className="border-default block w-16 rounded-md py-3 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
defaultValue={eventType.periodDays || 30}
|
defaultValue={eventType.periodDays || 30}
|
||||||
/>
|
/>
|
||||||
<select
|
<Select
|
||||||
id=""
|
options={optionsPeriod}
|
||||||
className="border-default bg-default text-default block h-9 w-full rounded-md py-2 pl-3 pr-10 text-sm focus:outline-none"
|
isSearchable={false}
|
||||||
{...formMethods.register("periodCountCalendarDays")}
|
isDisabled={periodTypeLocked.disabled}
|
||||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
onChange={(opt) => {
|
||||||
<option value="1">{t("calendar_days")}</option>
|
formMethods.setValue(
|
||||||
<option value="0">{t("business_days")}</option>
|
"periodCountCalendarDays",
|
||||||
</select>
|
opt?.value.toString() as "0" | "1"
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={
|
||||||
|
optionsPeriod.find(
|
||||||
|
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
||||||
|
) ?? optionsPeriod[0]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{period.type === "RANGE" && (
|
{period.type === "RANGE" && (
|
||||||
@@ -362,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
startDate={formMethods.getValues("periodDates").startDate}
|
startDate={formMethods.getValues("periodDates").startDate}
|
||||||
endDate={formMethods.getValues("periodDates").endDate}
|
endDate={formMethods.getValues("periodDates").endDate}
|
||||||
|
disabled={periodTypeLocked.disabled}
|
||||||
onDatesChange={({ startDate, endDate }) => {
|
onDatesChange={({ startDate, endDate }) => {
|
||||||
formMethods.setValue("periodDates", {
|
formMethods.setValue("periodDates", {
|
||||||
startDate,
|
startDate,
|
||||||
@@ -400,6 +451,7 @@ type IntervalLimitItemProps = {
|
|||||||
step: number;
|
step: number;
|
||||||
value: number;
|
value: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||||
hasDeleteButton?: boolean;
|
hasDeleteButton?: boolean;
|
||||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||||
@@ -414,6 +466,7 @@ const IntervalLimitItem = ({
|
|||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
selectOptions,
|
selectOptions,
|
||||||
hasDeleteButton,
|
hasDeleteButton,
|
||||||
|
disabled,
|
||||||
onDelete,
|
onDelete,
|
||||||
onLimitChange,
|
onLimitChange,
|
||||||
onIntervalSelect,
|
onIntervalSelect,
|
||||||
@@ -424,8 +477,9 @@ const IntervalLimitItem = ({
|
|||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||||
className="mb-0"
|
className="mb-0 !h-auto"
|
||||||
placeholder={`${value}`}
|
placeholder={`${value}`}
|
||||||
|
disabled={disabled}
|
||||||
min={step}
|
min={step}
|
||||||
step={step}
|
step={step}
|
||||||
defaultValue={value}
|
defaultValue={value}
|
||||||
@@ -435,11 +489,12 @@ const IntervalLimitItem = ({
|
|||||||
<Select
|
<Select
|
||||||
options={selectOptions}
|
options={selectOptions}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
|
isDisabled={disabled}
|
||||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||||
onChange={onIntervalSelect}
|
onChange={onIntervalSelect}
|
||||||
/>
|
/>
|
||||||
{hasDeleteButton && (
|
{hasDeleteButton && !disabled && (
|
||||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
@@ -450,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
|
|||||||
defaultLimit: number;
|
defaultLimit: number;
|
||||||
step: number;
|
step: number;
|
||||||
textFieldSuffix?: string;
|
textFieldSuffix?: string;
|
||||||
|
disabled?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||||
@@ -457,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
defaultLimit,
|
defaultLimit,
|
||||||
step,
|
step,
|
||||||
textFieldSuffix,
|
textFieldSuffix,
|
||||||
|
disabled,
|
||||||
}: IntervalLimitsManagerProps<K>) => {
|
}: IntervalLimitsManagerProps<K>) => {
|
||||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||||
const watchIntervalLimits = watch(propertyName);
|
const watchIntervalLimits = watch(propertyName);
|
||||||
@@ -506,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
limitKey={limitKey}
|
limitKey={limitKey}
|
||||||
step={step}
|
step={step}
|
||||||
value={value}
|
value={value}
|
||||||
|
disabled={disabled}
|
||||||
textFieldSuffix={textFieldSuffix}
|
textFieldSuffix={textFieldSuffix}
|
||||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||||
@@ -536,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
|||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
|
||||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
|
||||||
{t("add_limit")}
|
{t("add_limit")}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="">
|
<div className="">
|
||||||
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
|
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
|||||||
|
|
||||||
import type { EventLocationType } from "@calcom/app-store/locations";
|
import type { EventLocationType } from "@calcom/app-store/locations";
|
||||||
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
import { getEventLocationType, MeetLocationType, LocationType } from "@calcom/app-store/locations";
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
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 { md } from "@calcom/lib/markdownIt";
|
import { md } from "@calcom/lib/markdownIt";
|
||||||
@@ -27,7 +28,7 @@ import {
|
|||||||
SkeletonContainer,
|
SkeletonContainer,
|
||||||
SkeletonText,
|
SkeletonText,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiEdit2, FiCheck, FiX, FiPlus } from "@calcom/ui/components/icon";
|
import { Edit2, Check, X, Plus } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||||
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
import type { SingleValueLocationOption, LocationOption } from "@components/ui/form/LocationSelect";
|
||||||
@@ -45,8 +46,33 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||||
|
const locationAvailable =
|
||||||
|
props.eventType.locations &&
|
||||||
|
props.eventType.locations.length > 0 &&
|
||||||
|
props.locationOptions.some((op) =>
|
||||||
|
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
|
||||||
|
);
|
||||||
|
const locationDetails = props.eventType.locations &&
|
||||||
|
props.eventType.locations.length > 0 &&
|
||||||
|
!locationAvailable && {
|
||||||
|
slug: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", "-")
|
||||||
|
.replace("_video", ""),
|
||||||
|
name: props.eventType.locations[0].type
|
||||||
|
.replace("integrations:", "")
|
||||||
|
.replace(":", " ")
|
||||||
|
.replace("_video", "")
|
||||||
|
.split(" ")
|
||||||
|
.map((word) => word[0].toUpperCase() + word.slice(1))
|
||||||
|
.join(" "),
|
||||||
|
};
|
||||||
|
return { locationAvailable, locationDetails };
|
||||||
|
};
|
||||||
interface DescriptionEditorProps {
|
interface DescriptionEditorProps {
|
||||||
description?: string | null;
|
description?: string | null;
|
||||||
|
editable?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||||
@@ -64,6 +90,7 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
|||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
placeholder={t("quick_video_meeting")}
|
placeholder={t("quick_video_meeting")}
|
||||||
|
editable={props.editable}
|
||||||
/>
|
/>
|
||||||
) : (
|
) : (
|
||||||
<SkeletonContainer>
|
<SkeletonContainer>
|
||||||
@@ -174,6 +201,13 @@ export const EventSetupTab = (
|
|||||||
resolver: zodResolver(locationFormSchema),
|
resolver: zodResolver(locationFormSchema),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||||
|
useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
const Locations = () => {
|
const Locations = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
@@ -188,6 +222,12 @@ export const EventSetupTab = (
|
|||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const defaultValue = isManagedEventType
|
||||||
|
? locationOptions.find((op) => op.label === t("default"))?.options[0]
|
||||||
|
: undefined;
|
||||||
|
|
||||||
|
const { locationDetails, locationAvailable } = getLocationInfo(props);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
{validLocations.length === 0 && (
|
{validLocations.length === 0 && (
|
||||||
@@ -195,6 +235,8 @@ export const EventSetupTab = (
|
|||||||
<LocationSelect
|
<LocationSelect
|
||||||
placeholder={t("select")}
|
placeholder={t("select")}
|
||||||
options={locationOptions}
|
options={locationOptions}
|
||||||
|
isDisabled={shouldLockDisableProps("locations").disabled}
|
||||||
|
defaultValue={defaultValue}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||||
menuPlacement="auto"
|
menuPlacement="auto"
|
||||||
@@ -253,10 +295,10 @@ export const EventSetupTab = (
|
|||||||
}}
|
}}
|
||||||
aria-label={t("edit")}
|
aria-label={t("edit")}
|
||||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||||
<FiEdit2 className="h-4 w-4" />
|
<Edit2 className="h-4 w-4" />
|
||||||
</button>
|
</button>
|
||||||
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
<button type="button" onClick={() => removeLocation(location)} aria-label={t("remove")}>
|
||||||
<FiX className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
|
<X className="border-l-1 hover:text-emphasis text-subtle h-6 w-6 pl-1 " />
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -268,7 +310,7 @@ export const EventSetupTab = (
|
|||||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||||
) && (
|
) && (
|
||||||
<div className="text-default flex text-sm">
|
<div className="text-default flex text-sm">
|
||||||
<FiCheck className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
<Check className="mt-0.5 mr-1.5 h-2 w-2.5" />
|
||||||
<Trans i18nKey="event_type_requres_google_cal">
|
<Trans i18nKey="event_type_requres_google_cal">
|
||||||
<p>
|
<p>
|
||||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
@@ -282,11 +324,19 @@ export const EventSetupTab = (
|
|||||||
</Trans>
|
</Trans>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{validLocations.length > 0 && (
|
{isChildrenManagedEventType && !locationAvailable && locationDetails && (
|
||||||
|
<p className="pl-1 text-sm leading-none text-red-600">
|
||||||
|
{t("app_not_connected", { appName: locationDetails.name })}{" "}
|
||||||
|
<a className="underline" href={`${CAL_URL}/apps/${locationDetails.slug}`}>
|
||||||
|
{t("connect_now")}
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{validLocations.length > 0 && !isManagedEventType && !isChildrenManagedEventType && (
|
||||||
<li>
|
<li>
|
||||||
<Button
|
<Button
|
||||||
data-testid="add-location"
|
data-testid="add-location"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
onClick={() => setShowLocationModal(true)}>
|
onClick={() => setShowLocationModal(true)}>
|
||||||
{t("add_location")}
|
{t("add_location")}
|
||||||
@@ -299,22 +349,33 @@ export const EventSetupTab = (
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const lengthLockedProps = shouldLockDisableProps("length");
|
||||||
|
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div className="space-y-8">
|
<div className="space-y-8">
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("title")}
|
label={t("title")}
|
||||||
|
{...shouldLockDisableProps("title")}
|
||||||
defaultValue={eventType.title}
|
defaultValue={eventType.title}
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
<Label>{t("description")}</Label>
|
<Label>
|
||||||
<DescriptionEditor description={eventType?.description} />
|
{t("description")}
|
||||||
|
{shouldLockIndicator("description")}
|
||||||
|
</Label>
|
||||||
|
<DescriptionEditor
|
||||||
|
description={eventType?.description}
|
||||||
|
editable={!descriptionLockedProps.disabled}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
|
{...shouldLockDisableProps("slug")}
|
||||||
defaultValue={eventType.slug}
|
defaultValue={eventType.slug}
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
@@ -367,12 +428,14 @@ export const EventSetupTab = (
|
|||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("default_duration")}
|
{t("default_duration")}
|
||||||
|
{shouldLockIndicator("length")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Select
|
<Select
|
||||||
value={defaultDuration}
|
value={defaultDuration}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
name="length"
|
name="length"
|
||||||
className="text-sm"
|
className="text-sm"
|
||||||
|
isDisabled={lengthLockedProps.disabled}
|
||||||
noOptionsMessage={() => t("default_duration_no_options")}
|
noOptionsMessage={() => t("default_duration_no_options")}
|
||||||
options={selectedMultipleDuration}
|
options={selectedMultipleDuration}
|
||||||
onChange={(option) => {
|
onChange={(option) => {
|
||||||
@@ -388,32 +451,36 @@ export const EventSetupTab = (
|
|||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
type="number"
|
type="number"
|
||||||
|
{...lengthLockedProps}
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={eventType.length ?? 15}
|
||||||
{...formMethods.register("length")}
|
{...formMethods.register("length")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
{!lengthLockedProps.disabled && (
|
||||||
<SettingsToggle
|
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||||
title={t("allow_booker_to_select_duration")}
|
<SettingsToggle
|
||||||
checked={multipleDuration !== undefined}
|
title={t("allow_booker_to_select_duration")}
|
||||||
onCheckedChange={() => {
|
checked={multipleDuration !== undefined}
|
||||||
if (multipleDuration !== undefined) {
|
onCheckedChange={() => {
|
||||||
setMultipleDuration(undefined);
|
if (multipleDuration !== undefined) {
|
||||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
setMultipleDuration(undefined);
|
||||||
formMethods.setValue("length", eventType.length);
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
} else {
|
formMethods.setValue("length", eventType.length);
|
||||||
setMultipleDuration([]);
|
} else {
|
||||||
formMethods.setValue("metadata.multipleDuration", []);
|
setMultipleDuration([]);
|
||||||
formMethods.setValue("length", 0);
|
formMethods.setValue("metadata.multipleDuration", []);
|
||||||
}
|
formMethods.setValue("length", 0);
|
||||||
}}
|
}
|
||||||
/>
|
}}
|
||||||
</div>
|
/>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
<div>
|
<div>
|
||||||
<Skeleton as={Label} loadingClassName="w-16">
|
<Skeleton as={Label} loadingClassName="w-16">
|
||||||
{t("location")}
|
{t("location")}
|
||||||
|
{shouldLockIndicator("locations")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { SchedulingType } from "@prisma/client";
|
import { SchedulingType } from "@prisma/client";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useEffect, useRef } from "react";
|
import { useEffect, useRef } from "react";
|
||||||
import type { ComponentProps } from "react";
|
import type { ComponentProps } from "react";
|
||||||
@@ -7,24 +7,46 @@ import type { Options } from "react-select";
|
|||||||
|
|
||||||
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import type { CheckedSelectOption } from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
import CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||||
|
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||||
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 { Label, Select } from "@calcom/ui";
|
import { Label, Select } from "@calcom/ui";
|
||||||
|
|
||||||
interface IMemberToValue {
|
interface IUserToValue {
|
||||||
id: number | null;
|
id: number | null;
|
||||||
name: string | null;
|
name: string | null;
|
||||||
username: string | null;
|
username: string | null;
|
||||||
email: string;
|
email: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
|
||||||
value: `${id || ""}`,
|
value: `${id || ""}`,
|
||||||
label: `${name || ""}`,
|
label: `${name || ""}`,
|
||||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||||
email,
|
email,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const mapMemberToChildrenOption = (
|
||||||
|
member: EventTypeSetupProps["teamMembers"][number],
|
||||||
|
slug: string
|
||||||
|
) => {
|
||||||
|
return {
|
||||||
|
slug,
|
||||||
|
hidden: false,
|
||||||
|
created: false,
|
||||||
|
owner: {
|
||||||
|
id: member.id,
|
||||||
|
name: member.name ?? "",
|
||||||
|
email: member.email,
|
||||||
|
username: member.username ?? "",
|
||||||
|
membership: member.membership,
|
||||||
|
eventTypeSlugs: member.eventTypes ?? [],
|
||||||
|
},
|
||||||
|
value: `${member.id ?? ""}`,
|
||||||
|
label: member.name ?? "",
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof mapUserToValue>) => {
|
||||||
if (a.label < b.label) {
|
if (a.label < b.label) {
|
||||||
return -1;
|
return -1;
|
||||||
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
|||||||
return 0;
|
return 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypesList = ({
|
||||||
|
options = [],
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
...rest
|
||||||
|
}: {
|
||||||
|
value: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
onChange?: (options: ReturnType<typeof mapMemberToChildrenOption>[]) => void;
|
||||||
|
options?: Options<ReturnType<typeof mapMemberToChildrenOption>>;
|
||||||
|
} & Omit<Partial<ComponentProps<typeof ChildrenEventTypeSelect>>, "onChange" | "value">) => {
|
||||||
|
const { t } = useLocale();
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col space-y-5">
|
||||||
|
<div>
|
||||||
|
<Label>{t("assign_to")}</Label>
|
||||||
|
<ChildrenEventTypeSelect
|
||||||
|
onChange={(options) => {
|
||||||
|
onChange &&
|
||||||
|
onChange(
|
||||||
|
options.map((option) => ({
|
||||||
|
...option,
|
||||||
|
}))
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
options={options.filter((opt) => !value.find((val) => val.owner.id.toString() === opt.value))}
|
||||||
|
controlShouldRenderValue={false}
|
||||||
|
{...rest}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const CheckedHostField = ({
|
const CheckedHostField = ({
|
||||||
labelText,
|
labelText,
|
||||||
placeholder,
|
placeholder,
|
||||||
@@ -123,6 +179,21 @@ const RoundRobinHosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const ChildrenEventTypes = ({
|
||||||
|
childrenEventTypeOptions,
|
||||||
|
}: {
|
||||||
|
childrenEventTypeOptions: ReturnType<typeof mapMemberToChildrenOption>[];
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<Controller<FormValues>
|
||||||
|
name="children"
|
||||||
|
render={({ field: { onChange, value } }) => (
|
||||||
|
<ChildrenEventTypesList value={value} options={childrenEventTypeOptions} onChange={onChange} />
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const Hosts = ({
|
const Hosts = ({
|
||||||
teamMembers,
|
teamMembers,
|
||||||
}: {
|
}: {
|
||||||
@@ -189,6 +260,7 @@ const Hosts = ({
|
|||||||
/>*/}
|
/>*/}
|
||||||
</>
|
</>
|
||||||
),
|
),
|
||||||
|
MANAGED: <></>,
|
||||||
};
|
};
|
||||||
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
return !!schedulingType ? schedulingTypeRender[schedulingType] : <></>;
|
||||||
}}
|
}}
|
||||||
@@ -196,7 +268,11 @@ const Hosts = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "teamMembers" | "team">) => {
|
export const EventTeamTab = ({
|
||||||
|
team,
|
||||||
|
teamMembers,
|
||||||
|
eventType,
|
||||||
|
}: Pick<EventTypeSetupProps, "teamMembers" | "team" | "eventType">) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const schedulingTypeOptions: {
|
const schedulingTypeOptions: {
|
||||||
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||||
|
const childrenEventTypeOptions = teamMembers.map((member) => {
|
||||||
|
return mapMemberToChildrenOption(member, eventType.slug);
|
||||||
|
});
|
||||||
|
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && (
|
{team && !isManagedEventType && (
|
||||||
<div className="space-y-5">
|
<div className="space-y-5">
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<Label>{t("scheduling_type")}</Label>
|
<Label>{t("scheduling_type")}</Label>
|
||||||
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
|||||||
<Hosts teamMembers={teamMembersOptions} />
|
<Hosts teamMembers={teamMembersOptions} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
{team && isManagedEventType && (
|
||||||
|
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -1,16 +1,17 @@
|
|||||||
|
import type { Webhook } from "@prisma/client";
|
||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { WebhookForm } from "@calcom/features/webhooks/components";
|
import { WebhookForm } from "@calcom/features/webhooks/components";
|
||||||
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
import type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Webhook } from "@calcom/prisma/client";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiPlus } from "@calcom/ui/components/icon";
|
import { Plus, Lock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const EventTeamWebhooksTab = ({
|
export const EventTeamWebhooksTab = ({
|
||||||
eventType,
|
eventType,
|
||||||
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
data-testid="new_webhook"
|
data-testid="new_webhook"
|
||||||
StartIcon={FiPlus}
|
StartIcon={Plus}
|
||||||
onClick={() => setCreateModalOpen(true)}>
|
onClick={() => setCreateModalOpen(true)}>
|
||||||
{t("new_webhook")}
|
{t("new_webhook")}
|
||||||
</Button>
|
</Button>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{team && webhooks && !isLoading && (
|
{team && webhooks && !isLoading && (
|
||||||
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
|
|||||||
<div>
|
<div>
|
||||||
<div>
|
<div>
|
||||||
<>
|
<>
|
||||||
|
{isManagedEventType && (
|
||||||
|
<Alert
|
||||||
|
severity="neutral"
|
||||||
|
className="mb-2"
|
||||||
|
title={t("locked_for_members")}
|
||||||
|
message={t("locked_webhooks_description")}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
<>
|
<>
|
||||||
<div className="mb-8 rounded-md border">
|
<div className="mb-2 rounded-md border">
|
||||||
{webhooks.map((webhook, index) => {
|
{webhooks.map((webhook, index) => {
|
||||||
return (
|
return (
|
||||||
<WebhookListItem
|
<WebhookListItem
|
||||||
key={webhook.id}
|
key={webhook.id}
|
||||||
webhook={webhook}
|
webhook={webhook}
|
||||||
lastItem={webhooks.length === index + 1}
|
lastItem={webhooks.length === index + 1}
|
||||||
|
canEditWebhook={!webhookLockedStatus.disabled}
|
||||||
onEditWebhook={() => {
|
onEditWebhook={() => {
|
||||||
setEditModalOpen(true);
|
setEditModalOpen(true);
|
||||||
setWebhookToEdit(webhook);
|
setWebhookToEdit(webhook);
|
||||||
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
|
|||||||
Icon={TbWebhook}
|
Icon={TbWebhook}
|
||||||
headline={t("create_your_first_webhook")}
|
headline={t("create_your_first_webhook")}
|
||||||
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
description={t("create_your_first_team_webhook_description", { appName: APP_NAME })}
|
||||||
buttonRaw={<NewWebhookButton />}
|
buttonRaw={
|
||||||
|
isChildrenManagedEventType && !isManagedEventType ? (
|
||||||
|
<Button StartIcon={Lock} color="secondary" disabled>
|
||||||
|
{t("locked_by_admin")}
|
||||||
|
</Button>
|
||||||
|
) : (
|
||||||
|
<NewWebhookButton />
|
||||||
|
)
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -1,10 +1,13 @@
|
|||||||
|
import { SchedulingType } from "@prisma/client";
|
||||||
|
import { Webhook as TbWebhook } from "lucide-react";
|
||||||
import type { TFunction } from "next-i18next";
|
import type { TFunction } from "next-i18next";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||||
import { useMemo, useState, Suspense } from "react";
|
import { useMemo, useState, Suspense } from "react";
|
||||||
import type { UseFormReturn } from "react-hook-form";
|
import type { UseFormReturn } from "react-hook-form";
|
||||||
import { TbWebhook } from "react-icons/tb";
|
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { CAL_URL } from "@calcom/lib/constants";
|
import { CAL_URL } from "@calcom/lib/constants";
|
||||||
@@ -32,19 +35,19 @@ import {
|
|||||||
VerticalTabs,
|
VerticalTabs,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiLink,
|
Link as LinkIcon,
|
||||||
FiCalendar,
|
Calendar,
|
||||||
FiClock,
|
Clock,
|
||||||
FiSliders,
|
Sliders,
|
||||||
FiRepeat,
|
Repeat,
|
||||||
FiGrid,
|
Grid,
|
||||||
FiZap,
|
Zap,
|
||||||
FiUsers,
|
Users,
|
||||||
FiExternalLink,
|
ExternalLink,
|
||||||
FiCode,
|
Code,
|
||||||
FiTrash,
|
Trash,
|
||||||
FiMoreHorizontal,
|
MoreHorizontal,
|
||||||
FiLoader,
|
Loader,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
@@ -77,44 +80,38 @@ function getNavigation(props: {
|
|||||||
{
|
{
|
||||||
name: "event_setup_tab_title",
|
name: "event_setup_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||||
icon: FiLink,
|
icon: LinkIcon,
|
||||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
|
||||||
name: "availability",
|
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
|
||||||
icon: FiCalendar,
|
|
||||||
info: `default_schedule_name`, // TODO: Get this from props
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||||
icon: FiClock,
|
icon: Clock,
|
||||||
info: `event_limit_tab_description`,
|
info: `event_limit_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_advanced_tab_title",
|
name: "event_advanced_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||||
icon: FiSliders,
|
icon: Sliders,
|
||||||
info: `event_advanced_tab_description`,
|
info: `event_advanced_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recurring",
|
name: "recurring",
|
||||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||||
icon: FiRepeat,
|
icon: Repeat,
|
||||||
info: `recurring_event_tab_description`,
|
info: `recurring_event_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "apps",
|
name: "apps",
|
||||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||||
icon: FiGrid,
|
icon: Grid,
|
||||||
//TODO: Handle proper translation with count handling
|
//TODO: Handle proper translation with count handling
|
||||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "workflows",
|
name: "workflows",
|
||||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||||
icon: FiZap,
|
icon: Zap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
|
|
||||||
const hasPermsToDelete = currentUserMembership?.role !== "MEMBER" || !currentUserMembership;
|
const hasPermsToDelete =
|
||||||
|
currentUserMembership?.role !== "MEMBER" ||
|
||||||
|
!currentUserMembership ||
|
||||||
|
eventType.schedulingType === SchedulingType.MANAGED;
|
||||||
|
|
||||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
@@ -157,23 +157,55 @@ function EventTypeSingleLayout({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
const navigation = getNavigation({
|
let navigation = getNavigation({
|
||||||
t,
|
t,
|
||||||
eventType,
|
eventType,
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
});
|
});
|
||||||
|
navigation.splice(1, 0, {
|
||||||
|
name: "availability",
|
||||||
|
href: `/event-types/${eventType.id}?tabName=availability`,
|
||||||
|
icon: Calendar,
|
||||||
|
info:
|
||||||
|
isManagedEventType || isChildrenManagedEventType
|
||||||
|
? eventType.schedule === null
|
||||||
|
? "Member's default schedule"
|
||||||
|
: isChildrenManagedEventType
|
||||||
|
? `${
|
||||||
|
eventType.scheduleName
|
||||||
|
? `${eventType.scheduleName} - ${t("managed")}`
|
||||||
|
: `default_schedule_name`
|
||||||
|
}`
|
||||||
|
: eventType.scheduleName ?? `default_schedule_name`
|
||||||
|
: `default_schedule_name`,
|
||||||
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "assignment",
|
name: "assignment",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${eventType.id}?tabName=team`,
|
||||||
icon: FiUsers,
|
icon: Users,
|
||||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
||||||
|
isManagedEventType
|
||||||
|
? ` - ${t("count_members", { count: formMethods.watch("children").length || 0 })}`
|
||||||
|
: ""
|
||||||
|
}`,
|
||||||
});
|
});
|
||||||
|
}
|
||||||
|
if (isManagedEventType || isChildrenManagedEventType) {
|
||||||
|
// Removing apps and workflows for manageg event types by admins v1
|
||||||
|
navigation = navigation.slice(0, -2);
|
||||||
|
} else {
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
||||||
@@ -189,6 +221,7 @@ function EventTypeSingleLayout({
|
|||||||
}`;
|
}`;
|
||||||
|
|
||||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
||||||
|
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
@@ -197,77 +230,87 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
|
{!eventType.metadata.managedEventConfig && (
|
||||||
<Skeleton
|
<>
|
||||||
as={Label}
|
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
|
||||||
htmlFor="hiddenSwitch"
|
<Skeleton
|
||||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
as={Label}
|
||||||
{t("hide_from_profile")}
|
htmlFor="hiddenSwitch"
|
||||||
</Skeleton>
|
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||||
<Switch
|
{t("hide_from_profile")}
|
||||||
id="hiddenSwitch"
|
</Skeleton>
|
||||||
checked={formMethods.watch("hidden")}
|
<Switch
|
||||||
onCheckedChange={(e) => {
|
id="hiddenSwitch"
|
||||||
formMethods.setValue("hidden", e);
|
checked={formMethods.watch("hidden")}
|
||||||
}}
|
onCheckedChange={(e) => {
|
||||||
/>
|
formMethods.setValue("hidden", e);
|
||||||
</div>
|
}}
|
||||||
<VerticalDivider className="hidden lg:block" />
|
/>
|
||||||
|
</div>
|
||||||
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
||||||
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
|
<ButtonGroup combined containerProps={{ className: "border-default hidden lg:flex" }}>
|
||||||
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
{!isManagedEventType && (
|
||||||
<Tooltip content={t("preview")}>
|
<>
|
||||||
<Button
|
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
||||||
color="secondary"
|
<Tooltip content={t("preview")}>
|
||||||
data-testid="preview-button"
|
<Button
|
||||||
target="_blank"
|
color="secondary"
|
||||||
variant="icon"
|
data-testid="preview-button"
|
||||||
href={permalink}
|
target="_blank"
|
||||||
rel="noreferrer"
|
variant="icon"
|
||||||
StartIcon={FiExternalLink}
|
href={permalink}
|
||||||
/>
|
rel="noreferrer"
|
||||||
</Tooltip>
|
StartIcon={ExternalLink}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
tooltip={t("copy_link")}
|
tooltip={t("copy_link")}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<EmbedButton
|
<EmbedButton
|
||||||
embedUrl={encodeURIComponent(embedLink)}
|
embedUrl={encodeURIComponent(embedLink)}
|
||||||
StartIcon={FiCode}
|
StartIcon={Code}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
/>
|
/>
|
||||||
<Button
|
</>
|
||||||
color="destructive"
|
)}
|
||||||
variant="icon"
|
{!isChildrenManagedEventType && (
|
||||||
StartIcon={FiTrash}
|
<Button
|
||||||
tooltip={t("delete")}
|
color="destructive"
|
||||||
disabled={!hasPermsToDelete}
|
variant="icon"
|
||||||
onClick={() => setDeleteDialogOpen(true)}
|
StartIcon={Trash}
|
||||||
/>
|
tooltip={t("delete")}
|
||||||
|
disabled={!hasPermsToDelete}
|
||||||
|
onClick={() => setDeleteDialogOpen(true)}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
|
|
||||||
<VerticalDivider className="hidden lg:block" />
|
<VerticalDivider className="hidden lg:block" />
|
||||||
|
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||||
<DropdownMenuItem className="focus:ring-muted">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
target="_blank"
|
target="_blank"
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiExternalLink}
|
StartIcon={ExternalLink}
|
||||||
href={permalink}
|
href={permalink}
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
@@ -276,7 +319,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownMenuItem className="focus:ring-muted">
|
<DropdownMenuItem className="focus:ring-muted">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(permalink);
|
navigator.clipboard.writeText(permalink);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
@@ -288,7 +331,7 @@ function EventTypeSingleLayout({
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
color="destructive"
|
color="destructive"
|
||||||
StartIcon={FiTrash}
|
StartIcon={Trash}
|
||||||
disabled={!hasPermsToDelete}
|
disabled={!hasPermsToDelete}
|
||||||
onClick={() => setDeleteDialogOpen(true)}>
|
onClick={() => setDeleteDialogOpen(true)}>
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
|
|||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
<Suspense fallback={<FiLoader />}>
|
<Suspense fallback={<Loader />}>
|
||||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||||
<div className="hidden xl:block">
|
<div className="hidden xl:block">
|
||||||
<VerticalTabs
|
<VerticalTabs
|
||||||
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
|
|||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isLoading={deleteMutation.isLoading}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_event_type")}
|
title={t(`delete${isManagedEvent}_event_type`)}
|
||||||
confirmBtnText={t("confirm_delete_event_type")}
|
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||||
loadingText={t("confirm_delete_event_type")}
|
loadingText={t(`confirm_delete_event_type`)}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteMutation.mutate({ id: eventType.id });
|
deleteMutation.mutate({ id: eventType.id });
|
||||||
}}>
|
}}>
|
||||||
{t("delete_event_type_description")}
|
<p className="mt-5">
|
||||||
|
<Trans
|
||||||
|
i18nKey={`delete${isManagedEvent}_event_type_description`}
|
||||||
|
components={{ li: <li />, ul: <ul className="ml-4 list-disc" /> }}>
|
||||||
|
<ul>
|
||||||
|
<li>Members assigned to this event type will also have their event types deleted.</li>
|
||||||
|
<li>
|
||||||
|
Anyone who they've shared their link with will no longer be able to book using it.
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</Trans>
|
||||||
|
</p>
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
<EmbedDialog />
|
<EmbedDialog />
|
||||||
|
|||||||
@@ -1,23 +1,26 @@
|
|||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useFormContext } from "react-hook-form";
|
import { useFormContext } from "react-hook-form";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
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, Select, SettingsToggle } from "@calcom/ui";
|
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
type RecurringEventControllerProps = {
|
type RecurringEventControllerProps = {
|
||||||
recurringEvent: RecurringEvent | null;
|
eventType: EventTypeSetup;
|
||||||
paymentEnabled: boolean;
|
paymentEnabled: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RecurringEventController({
|
export default function RecurringEventController({
|
||||||
recurringEvent,
|
eventType,
|
||||||
paymentEnabled,
|
paymentEnabled,
|
||||||
}: RecurringEventControllerProps) {
|
}: RecurringEventControllerProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
|
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||||
|
eventType.recurringEvent
|
||||||
|
);
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||||
@@ -28,6 +31,14 @@ export default function RecurringEventController({
|
|||||||
value: value.toString(),
|
value: value.toString(),
|
||||||
}));
|
}));
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
|
||||||
|
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className={!paymentEnabled ? "w-full" : ""}>
|
<div className={!paymentEnabled ? "w-full" : ""}>
|
||||||
@@ -37,6 +48,7 @@ export default function RecurringEventController({
|
|||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("recurring_event")}
|
title={t("recurring_event")}
|
||||||
|
{...recurringLocked}
|
||||||
description={t("recurring_event_description")}
|
description={t("recurring_event_description")}
|
||||||
checked={recurringEventState !== null}
|
checked={recurringEventState !== null}
|
||||||
data-testid="recurring-event-check"
|
data-testid="recurring-event-check"
|
||||||
@@ -45,7 +57,7 @@ export default function RecurringEventController({
|
|||||||
formMethods.setValue("recurringEvent", null);
|
formMethods.setValue("recurringEvent", null);
|
||||||
setRecurringEventState(null);
|
setRecurringEventState(null);
|
||||||
} else {
|
} else {
|
||||||
const newVal = recurringEvent || {
|
const newVal = eventType.recurringEvent || {
|
||||||
interval: 1,
|
interval: 1,
|
||||||
count: 12,
|
count: 12,
|
||||||
freq: Frequency.WEEKLY,
|
freq: Frequency.WEEKLY,
|
||||||
@@ -58,11 +70,12 @@ export default function RecurringEventController({
|
|||||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="border-default bg-default text-default block h-[36px] w-16 rounded-md text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="mb-0"
|
||||||
defaultValue={recurringEventState.interval}
|
defaultValue={recurringEventState.interval}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
@@ -77,7 +90,8 @@ export default function RecurringEventController({
|
|||||||
options={recurringEventFreqOptions}
|
options={recurringEventFreqOptions}
|
||||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
className="w-18 block h-[36px] min-w-0 rounded-md text-sm"
|
className="w-18 ml-2 block min-w-0 rounded-md text-sm"
|
||||||
|
isDisabled={recurringLocked.disabled}
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -90,12 +104,13 @@ export default function RecurringEventController({
|
|||||||
</div>
|
</div>
|
||||||
<div className="mt-4 flex items-center">
|
<div className="mt-4 flex items-center">
|
||||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||||
<input
|
<TextField
|
||||||
|
disabled={recurringLocked.disabled}
|
||||||
type="number"
|
type="number"
|
||||||
min="1"
|
min="1"
|
||||||
max="20"
|
max="20"
|
||||||
className="border-default bg-default text-default block h-[36px] w-16 rounded-md text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
|
||||||
defaultValue={recurringEventState.count}
|
defaultValue={recurringEventState.count}
|
||||||
|
className="mb-0"
|
||||||
onChange={(event) => {
|
onChange={(event) => {
|
||||||
const newVal = {
|
const newVal = {
|
||||||
...recurringEventState,
|
...recurringEventState,
|
||||||
@@ -105,7 +120,7 @@ export default function RecurringEventController({
|
|||||||
setRecurringEventState(newVal);
|
setRecurringEventState(newVal);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">
|
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
|
||||||
{t("events", {
|
{t("events", {
|
||||||
count: recurringEventState.count,
|
count: recurringEventState.count,
|
||||||
})}
|
})}
|
||||||
|
|||||||
@@ -1,25 +1,29 @@
|
|||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import type { UnitTypeLongPlural } from "dayjs";
|
import type { UnitTypeLongPlural } from "dayjs";
|
||||||
import { Trans } from "next-i18next";
|
import { Trans } from "next-i18next";
|
||||||
import type { FormValues } from "pages/event-types/[type]";
|
import type { EventTypeSetup, FormValues } from "pages/event-types/[type]";
|
||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { Controller, useFormContext } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import type z from "zod";
|
import type z from "zod";
|
||||||
|
|
||||||
|
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||||
|
import { classNames } from "@calcom/lib";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Input, Label, SettingsToggle, RadioField } from "@calcom/ui";
|
import { Input, SettingsToggle, RadioField, Select } from "@calcom/ui";
|
||||||
|
|
||||||
type RequiresConfirmationControllerProps = {
|
type RequiresConfirmationControllerProps = {
|
||||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||||
requiresConfirmation: boolean;
|
requiresConfirmation: boolean;
|
||||||
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||||
seatsEnabled: boolean;
|
seatsEnabled: boolean;
|
||||||
|
eventType: EventTypeSetup;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default function RequiresConfirmationController({
|
export default function RequiresConfirmationController({
|
||||||
metadata,
|
metadata,
|
||||||
|
eventType,
|
||||||
requiresConfirmation,
|
requiresConfirmation,
|
||||||
onRequiresConfirmation,
|
onRequiresConfirmation,
|
||||||
seatsEnabled,
|
seatsEnabled,
|
||||||
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
|
|||||||
}
|
}
|
||||||
}, [requiresConfirmation]);
|
}, [requiresConfirmation]);
|
||||||
|
|
||||||
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
|
eventType,
|
||||||
|
t("locked_fields_admin_description"),
|
||||||
|
t("locked_fields_member_description")
|
||||||
|
);
|
||||||
|
const requiresConfirmationLockedProps = shouldLockDisableProps("requiresConfirmation");
|
||||||
|
|
||||||
|
const options = [
|
||||||
|
{ label: t("minute_timeUnit"), value: "minutes" },
|
||||||
|
{ label: t("hour_timeUnit"), value: "hours" },
|
||||||
|
];
|
||||||
|
|
||||||
|
const defaultValue = options.find(
|
||||||
|
(opt) =>
|
||||||
|
opt.value === (metadata?.requiresConfirmationThreshold?.unit ?? defaultRequiresConfirmationSetup.unit)
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="block items-start sm:flex">
|
<div className="block items-start sm:flex">
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
|
|||||||
render={() => (
|
render={() => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
title={t("requires_confirmation")}
|
title={t("requires_confirmation")}
|
||||||
disabled={seatsEnabled}
|
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
|
||||||
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
||||||
description={t("requires_confirmation_description")}
|
description={t("requires_confirmation_description")}
|
||||||
checked={requiresConfirmation}
|
checked={requiresConfirmation}
|
||||||
|
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||||
onCheckedChange={(val) => {
|
onCheckedChange={(val) => {
|
||||||
formMethods.setValue("requiresConfirmation", val);
|
formMethods.setValue("requiresConfirmation", val);
|
||||||
onRequiresConfirmation(val);
|
onRequiresConfirmation(val);
|
||||||
@@ -77,70 +99,82 @@ export default function RequiresConfirmationController({
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
}}>
|
}}>
|
||||||
<div className="flex flex-col flex-wrap justify-start gap-y-2 space-y-2">
|
<div className="flex flex-col flex-wrap justify-start gap-y-2">
|
||||||
<RadioField label={t("always_requires_confirmation")} id="always" value="always" />
|
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<RadioField
|
<RadioField
|
||||||
label={
|
label={t("always_requires_confirmation")}
|
||||||
<>
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
<Trans
|
id="always"
|
||||||
i18nKey="when_booked_with_less_than_notice"
|
value="always"
|
||||||
defaults="When booked with less than <time></time> notice"
|
/>
|
||||||
components={{
|
)}
|
||||||
time: (
|
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||||
<div className="mx-2 flex">
|
<RadioField
|
||||||
<Input
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
type="number"
|
className="items-center"
|
||||||
min={1}
|
label={
|
||||||
onChange={(evt) => {
|
<>
|
||||||
const val = Number(evt.target?.value);
|
<Trans
|
||||||
setRequiresConfirmationSetup({
|
i18nKey="when_booked_with_less_than_notice"
|
||||||
unit:
|
defaults="When booked with less than <time></time> notice"
|
||||||
requiresConfirmationSetup?.unit ??
|
components={{
|
||||||
defaultRequiresConfirmationSetup.unit,
|
time: (
|
||||||
time: val,
|
<div className="mx-2 inline-flex">
|
||||||
});
|
<Input
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
type="number"
|
||||||
}}
|
min={1}
|
||||||
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
disabled={requiresConfirmationLockedProps.disabled}
|
||||||
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
onChange={(evt) => {
|
||||||
/>
|
const val = Number(evt.target?.value);
|
||||||
<select
|
setRequiresConfirmationSetup({
|
||||||
onChange={(evt) => {
|
unit:
|
||||||
const val = evt.target.value as UnitTypeLongPlural;
|
requiresConfirmationSetup?.unit ??
|
||||||
setRequiresConfirmationSetup({
|
defaultRequiresConfirmationSetup.unit,
|
||||||
time:
|
time: val,
|
||||||
requiresConfirmationSetup?.time ??
|
});
|
||||||
defaultRequiresConfirmationSetup.time,
|
formMethods.setValue(
|
||||||
unit: val,
|
"metadata.requiresConfirmationThreshold.time",
|
||||||
});
|
val
|
||||||
formMethods.setValue("metadata.requiresConfirmationThreshold.unit", val);
|
);
|
||||||
}}
|
}}
|
||||||
className="border-default text-default bg-default ml-2 block h-9 rounded-md py-2 pl-3 pr-10 text-sm focus:outline-none"
|
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
||||||
defaultValue={
|
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||||
metadata?.requiresConfirmationThreshold?.unit ||
|
/>
|
||||||
defaultRequiresConfirmationSetup.unit
|
<label
|
||||||
}>
|
className={classNames(
|
||||||
<option value="minutes">{t("minute_timeUnit")}</option>
|
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
|
||||||
<option value="hours">{t("hour_timeUnit")}</option>
|
)}>
|
||||||
</select>
|
<Select
|
||||||
</div>
|
inputId="notice"
|
||||||
),
|
options={options}
|
||||||
}}
|
isSearchable={false}
|
||||||
/>
|
isDisabled={requiresConfirmationLockedProps.disabled}
|
||||||
</>
|
className="ml-2"
|
||||||
}
|
onChange={(opt) => {
|
||||||
id="notice"
|
setRequiresConfirmationSetup({
|
||||||
value="notice"
|
time:
|
||||||
/>
|
requiresConfirmationSetup?.time ??
|
||||||
<div className="flex items-center">
|
defaultRequiresConfirmationSetup.time,
|
||||||
<RadioGroup.Item
|
unit: opt?.value as UnitTypeLongPlural,
|
||||||
|
});
|
||||||
|
formMethods.setValue(
|
||||||
|
"metadata.requiresConfirmationThreshold.unit",
|
||||||
|
opt?.value as UnitTypeLongPlural
|
||||||
|
);
|
||||||
|
}}
|
||||||
|
defaultValue={defaultValue}
|
||||||
|
/>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
</>
|
||||||
|
}
|
||||||
id="notice"
|
id="notice"
|
||||||
value="notice"
|
value="notice"
|
||||||
className="min-w-4 bg-default flex h-4 w-4 cursor-pointer items-center rounded-full border border-black 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>
|
|
||||||
<Label htmlFor="notice" className="!m-0 flex items-center" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</RadioGroup.Root>
|
</RadioGroup.Root>
|
||||||
</SettingsToggle>
|
</SettingsToggle>
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
import { SkeletonAvatar, SkeletonContainer, SkeletonText } from "@calcom/ui";
|
||||||
import { FiClock, FiUser } from "@calcom/ui/components/icon";
|
import { Clock, User } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
function SkeletonLoader() {
|
function SkeletonLoader() {
|
||||||
return (
|
return (
|
||||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
|||||||
<div className="">
|
<div className="">
|
||||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiClock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
<Clock className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
<li className="flex items-center whitespace-nowrap">
|
<li className="flex items-center whitespace-nowrap">
|
||||||
<FiUser className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
<User className="text-subtle mt-0.5 mr-1.5 inline h-4 w-4" />
|
||||||
<SkeletonText className="h-4 w-12" />
|
<SkeletonText className="h-4 w-12" />
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { FiShield } from "@calcom/ui/components/icon";
|
import { Shield } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
const TwoFactorModalHeader = ({ title, description }: { title: string; description: string }) => {
|
||||||
return (
|
return (
|
||||||
<div className="mb-4 sm:flex sm:items-start">
|
<div className="mb-4 sm:flex sm:items-start">
|
||||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||||
<FiShield className="text-inverted h-6 w-6" />
|
<Shield className="text-inverted h-6 w-6" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
<div className="mt-3 text-center sm:mt-0 sm:ml-4 sm:text-left">
|
||||||
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
<h3 className="font-cal text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
|
|||||||
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 { Badge, showToast, Switch } from "@calcom/ui";
|
import { Badge, showToast, Switch } from "@calcom/ui";
|
||||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
import { ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export function CalendarSwitch(props: {
|
export function CalendarSwitch(props: {
|
||||||
type: string;
|
type: string;
|
||||||
@@ -75,8 +75,8 @@ export function CalendarSwitch(props: {
|
|||||||
/>
|
/>
|
||||||
{props.defaultSelected && (
|
{props.defaultSelected && (
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<div className="flex">
|
<div className="flex items-center">
|
||||||
<FiArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
|
<ArrowLeft className="text-default mr-1 h-4 w-4" /> {t("adding_events_to")}
|
||||||
</div>
|
</div>
|
||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
|||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
import { EmailField, EmptyScreen, Label, PasswordField, TextField } from "@calcom/ui";
|
||||||
import { FiUserCheck } from "@calcom/ui/components/icon";
|
import { UserCheck } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser> & { userCount: number }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
|
|||||||
props.onSuccess();
|
props.onSuccess();
|
||||||
}}>
|
}}>
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiUserCheck}
|
Icon={UserCheck}
|
||||||
headline={t("admin_user_created")}
|
headline={t("admin_user_created")}
|
||||||
description={t("admin_user_created_description")}
|
description={t("admin_user_created_description")}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterInputs, RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Button, TextField } from "@calcom/ui";
|
import { Button, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiExternalLink, FiLoader } from "@calcom/ui/components/icon";
|
import { Check, ExternalLink, Loader } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
type EnterpriseLicenseFormValues = {
|
type EnterpriseLicenseFormValues = {
|
||||||
licenseKey: string;
|
licenseKey: string;
|
||||||
@@ -91,7 +91,7 @@ const EnterpriseLicense = (
|
|||||||
<div>
|
<div>
|
||||||
<Button
|
<Button
|
||||||
className="w-full justify-center text-lg"
|
className="w-full justify-center text-lg"
|
||||||
EndIcon={FiExternalLink}
|
EndIcon={ExternalLink}
|
||||||
href="https://console.cal.com"
|
href="https://console.cal.com"
|
||||||
target="_blank">
|
target="_blank">
|
||||||
{t("purchase_license")}
|
{t("purchase_license")}
|
||||||
@@ -121,9 +121,9 @@ const EnterpriseLicense = (
|
|||||||
)}
|
)}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
checkLicenseLoading ? (
|
checkLicenseLoading ? (
|
||||||
<FiLoader className="h-5 w-5 animate-spin" />
|
<Loader className="h-5 w-5 animate-spin" />
|
||||||
) : errors.licenseKey === undefined && isDirty ? (
|
) : errors.licenseKey === undefined && isDirty ? (
|
||||||
<FiCheck className="h-5 w-5 text-green-700" />
|
<Check className="h-5 w-5 text-green-700" />
|
||||||
) : undefined
|
) : undefined
|
||||||
}
|
}
|
||||||
color={errors.licenseKey ? "warn" : ""}
|
color={errors.licenseKey ? "warn" : ""}
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
|||||||
import type { Dispatch, SetStateAction } from "react";
|
import type { Dispatch, SetStateAction } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { FiCheck } from "@calcom/ui/components/icon";
|
import { Check } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const StepDone = (props: {
|
const StepDone = (props: {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
@@ -24,7 +24,7 @@ const StepDone = (props: {
|
|||||||
}}>
|
}}>
|
||||||
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
<div className="min-h-36 my-6 flex flex-col items-center justify-center">
|
||||||
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
|
<div className="dark:bg-default flex h-[72px] w-[72px] items-center justify-center rounded-full bg-gray-600">
|
||||||
<FiCheck className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
<Check className="text-inverted dark:bg-default dark:text-default inline-block h-10 w-10" />
|
||||||
</div>
|
</div>
|
||||||
<div className="max-w-[420px] text-center">
|
<div className="max-w-[420px] text-center">
|
||||||
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
<h2 className="mt-6 mb-1 text-lg font-medium dark:text-gray-300">{t("all_done")}</h2>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import classNames from "classnames";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import type { ControllerRenderProps } from "react-hook-form";
|
import type { ControllerRenderProps } from "react-hook-form";
|
||||||
|
|
||||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
import { Edit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
const EditableHeading = function EditableHeading({
|
const EditableHeading = function EditableHeading({
|
||||||
value,
|
value,
|
||||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
|||||||
<label className="min-w-8 relative inline-block">
|
<label className="min-w-8 relative inline-block">
|
||||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||||
{!isEditing && isReady && (
|
{!isEditing && isReady && (
|
||||||
<FiEdit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
|
<Edit2 className=" text-subtle group-hover:text-subtle ml-1 -mt-px inline h-3 w-3" />
|
||||||
)}
|
)}
|
||||||
<input
|
<input
|
||||||
{...passThroughProps}
|
{...passThroughProps}
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { Tooltip } from "@calcom/ui";
|
import { Tooltip } from "@calcom/ui";
|
||||||
import { FiInfo } from "@calcom/ui/components/icon";
|
import { Info } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export default function InfoBadge({ content }: { content: string }) {
|
export default function InfoBadge({ content }: { content: string }) {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Tooltip side="top" content={content}>
|
<Tooltip side="top" content={content}>
|
||||||
<span title={content}>
|
<span title={content}>
|
||||||
<FiInfo className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
|
<Info className="text-subtle relative top-px left-1 right-1 mt-px h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</>
|
</>
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, Input, Label } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2, FiExternalLink } from "@calcom/ui/components/icon";
|
import { Check, Edit2, ExternalLink } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
export enum UsernameChangeStatusEnum {
|
export enum UsernameChangeStatusEnum {
|
||||||
UPGRADE = "UPGRADE",
|
UPGRADE = "UPGRADE",
|
||||||
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
usernameIsAvailable ? "" : ""
|
usernameIsAvailable ? "" : ""
|
||||||
)}>
|
)}>
|
||||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||||
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
|
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -251,7 +251,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent
|
<DialogContent
|
||||||
Icon={FiEdit2}
|
Icon={Edit2}
|
||||||
title={t("confirm_username_change_dialog_title")}
|
title={t("confirm_username_change_dialog_title")}
|
||||||
description={
|
description={
|
||||||
<>
|
<>
|
||||||
@@ -288,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
data-testid="go-to-billing"
|
data-testid="go-to-billing"
|
||||||
href={paymentLink}>
|
href={paymentLink}>
|
||||||
<>
|
<>
|
||||||
{t("go_to_stripe_billing")} <FiExternalLink className="ml-1 h-4 w-4" />
|
{t("go_to_stripe_billing")} <ExternalLink className="ml-1 h-4 w-4" />
|
||||||
</>
|
</>
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
|||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
|
import { Button, Dialog, DialogClose, DialogContent, TextField } from "@calcom/ui";
|
||||||
import { FiCheck, FiEdit2 } from "@calcom/ui/components/icon";
|
import { Check, Edit2 } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
interface ICustomUsernameProps {
|
interface ICustomUsernameProps {
|
||||||
currentUsername: string | undefined;
|
currentUsername: string | undefined;
|
||||||
@@ -79,7 +79,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
|
|
||||||
const ActionButtons = () => {
|
const ActionButtons = () => {
|
||||||
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
return usernameIsAvailable && currentUsername !== inputUsernameValue ? (
|
||||||
<div className="rlt:space-x-reverse ms-2 me-2 mt-px flex flex-row space-x-2 rtl:space-x-reverse">
|
<div className="ms-2 me-2 mt-px flex flex-row space-x-2">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => setOpenDialogSaveUsername(true)}
|
onClick={() => setOpenDialogSaveUsername(true)}
|
||||||
@@ -123,7 +123,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
autoCapitalize="none"
|
autoCapitalize="none"
|
||||||
autoCorrect="none"
|
autoCorrect="none"
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"mb-0 mt-0 rounded-md ltr:rounded-l-none rtl:rounded-r-none",
|
"mb-0 mt-0 rounded-md rounded-l-none",
|
||||||
markAsError
|
markAsError
|
||||||
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
? "focus:shadow-0 focus:ring-shadow-0 border-red-500 focus:border-red-500 focus:outline-none focus:ring-0"
|
||||||
: ""
|
: ""
|
||||||
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{currentUsername !== inputUsernameValue && (
|
{currentUsername !== inputUsernameValue && (
|
||||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||||
<span className={classNames("mx-2 py-2")}>
|
<span className={classNames("mx-2 py-2")}>
|
||||||
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
|
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -154,7 +154,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<Dialog open={openDialogSaveUsername}>
|
<Dialog open={openDialogSaveUsername}>
|
||||||
<DialogContent type="confirmation" Icon={FiEdit2} title={t("confirm_username_change_dialog_title")}>
|
<DialogContent type="confirmation" Icon={Edit2} title={t("confirm_username_change_dialog_title")}>
|
||||||
<div className="flex flex-row">
|
<div className="flex flex-row">
|
||||||
<div className="mb-4 w-full pt-1">
|
<div className="mb-4 w-full pt-1">
|
||||||
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
<div className="bg-subtle flex w-full flex-wrap rounded-sm py-3 text-sm">
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import type { Props } from "react-select";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Avatar } from "@calcom/ui";
|
import { Avatar } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import Select from "@components/ui/form/Select";
|
import Select from "@components/ui/form/Select";
|
||||||
|
|
||||||
@@ -49,7 +49,7 @@ export const CheckedSelect = ({
|
|||||||
alt={option.label}
|
alt={option.label}
|
||||||
/>
|
/>
|
||||||
{option.label}
|
{option.label}
|
||||||
<FiX
|
<X
|
||||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||||
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
|
className="text-subtle float-right mt-0.5 h-5 w-5 cursor-pointer"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import "react-calendar/dist/Calendar.css";
|
|||||||
import "react-date-picker/dist/DatePicker.css";
|
import "react-date-picker/dist/DatePicker.css";
|
||||||
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
import PrimitiveDatePicker from "react-date-picker/dist/entry.nostyle";
|
||||||
|
|
||||||
import { FiCalendar } from "@calcom/ui/components/icon";
|
import { Calendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import classNames from "@lib/classNames";
|
import classNames from "@lib/classNames";
|
||||||
|
|
||||||
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
|
|||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
clearIcon={null}
|
clearIcon={null}
|
||||||
calendarIcon={<FiCalendar className="text-subtle h-5 w-5" />}
|
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
|
||||||
value={date}
|
value={date}
|
||||||
minDate={minDate}
|
minDate={minDate}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
|
|||||||
@@ -27,8 +27,10 @@ const I18nextAdapter = appWithTranslation<NextJsAppProps<SSRConfig> & { children
|
|||||||
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
export type AppProps = Omit<NextAppProps<WithNonceProps & Record<string, unknown>>, "Component"> & {
|
||||||
Component: NextAppProps["Component"] & {
|
Component: NextAppProps["Component"] & {
|
||||||
requiresLicense?: boolean;
|
requiresLicense?: boolean;
|
||||||
isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean);
|
isThemeSupported?: boolean;
|
||||||
|
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||||
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
getLayout?: (page: React.ReactElement, router: NextRouter) => ReactNode;
|
||||||
|
PageWrapper?: (props: AppProps) => JSX.Element;
|
||||||
};
|
};
|
||||||
|
|
||||||
/** Will be defined only is there was an error */
|
/** Will be defined only is there was an error */
|
||||||
@@ -61,46 +63,78 @@ const CustomI18nextProvider = (props: AppPropsWithChildren) => {
|
|||||||
return <I18nextAdapter {...passedProps} />;
|
return <I18nextAdapter {...passedProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const enum ThemeSupport {
|
||||||
|
// e.g. Login Page
|
||||||
|
None = "none",
|
||||||
|
// Entire App except Booking Pages
|
||||||
|
App = "systemOnly",
|
||||||
|
// Booking Pages(including Routing Forms)
|
||||||
|
Booking = "userConfigured",
|
||||||
|
}
|
||||||
|
|
||||||
const CalcomThemeProvider = (
|
const CalcomThemeProvider = (
|
||||||
props: PropsWithChildren<
|
props: PropsWithChildren<
|
||||||
WithNonceProps & { isThemeSupported?: boolean | ((arg: { router: NextRouter }) => boolean) }
|
WithNonceProps & {
|
||||||
|
isBookingPage?: boolean | ((arg: { router: NextRouter }) => boolean);
|
||||||
|
isThemeSupported?: boolean;
|
||||||
|
}
|
||||||
>
|
>
|
||||||
) => {
|
) => {
|
||||||
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
// We now support the inverse of how we handled it in the past. Setting this to false will disable theme.
|
||||||
// undefined or true means we use system theme
|
// undefined or true means we use system theme
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const isThemeSupported = (() => {
|
const isBookingPage = (() => {
|
||||||
if (typeof props.isThemeSupported === "function") {
|
if (typeof props.isBookingPage === "function") {
|
||||||
return props.isThemeSupported({ router: router });
|
return props.isBookingPage({ router: router });
|
||||||
}
|
}
|
||||||
if (typeof props.isThemeSupported === "undefined") {
|
|
||||||
return true;
|
return props.isBookingPage;
|
||||||
}
|
|
||||||
return props.isThemeSupported;
|
|
||||||
})();
|
})();
|
||||||
|
|
||||||
const forcedTheme = !isThemeSupported ? "light" : undefined;
|
const themeSupport = isBookingPage
|
||||||
|
? ThemeSupport.Booking
|
||||||
|
: // if isThemeSupported is explicitly false, we don't use theme there
|
||||||
|
props.isThemeSupported === false
|
||||||
|
? ThemeSupport.None
|
||||||
|
: ThemeSupport.App;
|
||||||
|
|
||||||
|
const forcedTheme = themeSupport === ThemeSupport.None ? "light" : undefined;
|
||||||
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
// Use namespace of embed to ensure same namespaced embed are displayed with same theme. This allows different embeds on the same website to be themed differently
|
||||||
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
// One such example is our Embeds Demo and Testing page at http://localhost:3100
|
||||||
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
// Having `getEmbedNamespace` defined on window before react initializes the app, ensures that embedNamespace is available on the first mount and can be used as part of storageKey
|
||||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||||
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
|
const isEmbedMode = typeof embedNamespace === "string";
|
||||||
// This is done to ensure that the default theme is light when we force light mode and as soon as you navigate to a page that is dark we dont need a hard refresh to change
|
|
||||||
const storageKey =
|
const storageKey = isEmbedMode
|
||||||
typeof embedNamespace === "string"
|
? `embed-theme-${embedNamespace}`
|
||||||
? `embed-theme-${embedNamespace}`
|
: themeSupport === ThemeSupport.App
|
||||||
: !isThemeSupported
|
? "app-theme"
|
||||||
? "cal-light"
|
: themeSupport === ThemeSupport.Booking
|
||||||
: "theme";
|
? "booking-theme"
|
||||||
|
: undefined;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ThemeProvider
|
<ThemeProvider
|
||||||
nonce={props.nonce}
|
nonce={props.nonce}
|
||||||
enableColorScheme={false}
|
enableColorScheme={false}
|
||||||
enableSystem={isThemeSupported}
|
enableSystem={themeSupport !== ThemeSupport.None}
|
||||||
forcedTheme={forcedTheme}
|
forcedTheme={forcedTheme}
|
||||||
storageKey={storageKey}
|
storageKey={storageKey}
|
||||||
|
// next-themes doesn't listen to changes on storageKey. So we need to force a re-render when storageKey changes
|
||||||
|
// This is how login to dashboard soft navigation changes theme from light to dark
|
||||||
|
key={storageKey}
|
||||||
attribute="class">
|
attribute="class">
|
||||||
|
{/* Embed Mode can be detected reliably only on client side here as there can be static generated pages as well which can't determine if it's embed mode at backend */}
|
||||||
|
{/* color-scheme makes background:transparent not work in iframe which is required by embed. */}
|
||||||
|
{typeof window !== "undefined" && !isEmbedMode && (
|
||||||
|
<style jsx global>
|
||||||
|
{`
|
||||||
|
.dark {
|
||||||
|
color-scheme: dark;
|
||||||
|
}
|
||||||
|
`}
|
||||||
|
</style>
|
||||||
|
)}
|
||||||
{props.children}
|
{props.children}
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
);
|
);
|
||||||
@@ -121,10 +155,10 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
|||||||
<SessionProvider session={session || undefined}>
|
<SessionProvider session={session || undefined}>
|
||||||
<CustomI18nextProvider {...props}>
|
<CustomI18nextProvider {...props}>
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
{/* color-scheme makes background:transparent not work which is required by embed. We need to ensure next-theme adds color-scheme to `body` instead of `html`(https://github.com/pacocoursey/next-themes/blob/main/src/index.tsx#L74). Once that's done we can enable color-scheme support */}
|
|
||||||
<CalcomThemeProvider
|
<CalcomThemeProvider
|
||||||
nonce={props.pageProps.nonce}
|
nonce={props.pageProps.nonce}
|
||||||
isThemeSupported={props.Component.isThemeSupported}>
|
isThemeSupported={props.Component.isThemeSupported}
|
||||||
|
isBookingPage={props.Component.isBookingPage}>
|
||||||
<FeatureFlagsProvider>
|
<FeatureFlagsProvider>
|
||||||
<MetaProvider>{props.children}</MetaProvider>
|
<MetaProvider>{props.children}</MetaProvider>
|
||||||
</FeatureFlagsProvider>
|
</FeatureFlagsProvider>
|
||||||
|
|||||||
+1
-4
@@ -13,9 +13,6 @@ function getCspPolicy(nonce: string) {
|
|||||||
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
// We can remove 'unsafe-inline' from style-src when we add nonces to all style tags
|
||||||
// Maybe see how @next-safe/middleware does it if it's supported.
|
// Maybe see how @next-safe/middleware does it if it's supported.
|
||||||
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
const useNonStrictPolicy = CSP_POLICY === "non-strict";
|
||||||
const SENTRY_ENDPOINT = process.env.NEXT_PUBLIC_SENTRY_DSN
|
|
||||||
? new URL(process.env.NEXT_PUBLIC_SENTRY_DSN, "http://base_url").origin
|
|
||||||
: "";
|
|
||||||
|
|
||||||
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
// We add WEBAPP_URL to img-src because of booking pages, which end up loading images from app.cal.com on cal.com
|
||||||
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
// FIXME: Write a layer to extract out EventType Analytics tracking endpoints and add them to img-src or connect-src as needed. e.g. fathom, Google Analytics and others
|
||||||
@@ -36,7 +33,7 @@ function getCspPolicy(nonce: string) {
|
|||||||
} app.cal.com;
|
} app.cal.com;
|
||||||
font-src 'self';
|
font-src 'self';
|
||||||
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
img-src 'self' ${WEBAPP_URL} https://www.gravatar.com https://img.youtube.com https://eu.ui-avatars.com/api/ data:;
|
||||||
connect-src 'self' ${SENTRY_ENDPOINT}
|
connect-src 'self'
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+3
-21
@@ -1,6 +1,5 @@
|
|||||||
require("dotenv").config({ path: "../../.env" });
|
require("dotenv").config({ path: "../../.env" });
|
||||||
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
const CopyWebpackPlugin = require("copy-webpack-plugin");
|
||||||
const { withSentryConfig } = require("@sentry/nextjs");
|
|
||||||
const os = require("os");
|
const os = require("os");
|
||||||
|
|
||||||
const { withAxiom } = require("next-axiom");
|
const { withAxiom } = require("next-axiom");
|
||||||
@@ -92,11 +91,11 @@ const nextConfig = {
|
|||||||
"@calcom/prisma",
|
"@calcom/prisma",
|
||||||
"@calcom/trpc",
|
"@calcom/trpc",
|
||||||
"@calcom/ui",
|
"@calcom/ui",
|
||||||
|
"lucide-react",
|
||||||
],
|
],
|
||||||
modularizeImports: {
|
modularizeImports: {
|
||||||
"@calcom/ui/components/icon": {
|
"@calcom/ui/components/icon": {
|
||||||
transform: "@react-icons/all-files/fi/{{member}}",
|
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
|
||||||
skipDefaultConversion: true,
|
|
||||||
preventFullImport: true,
|
preventFullImport: true,
|
||||||
},
|
},
|
||||||
"@calcom/features/insights/components": {
|
"@calcom/features/insights/components": {
|
||||||
@@ -318,21 +317,4 @@ const nextConfig = {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
const sentryWebpackPluginOptions = {
|
module.exports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
||||||
silent: true, // Suppresses all logs
|
|
||||||
};
|
|
||||||
|
|
||||||
const moduleExports = () => plugins.reduce((acc, next) => next(acc), nextConfig);
|
|
||||||
|
|
||||||
if (process.env.NEXT_PUBLIC_SENTRY_DSN) {
|
|
||||||
nextConfig.sentry = {
|
|
||||||
hideSourceMaps: true,
|
|
||||||
// Prevents Sentry from running on this Edge function, where Sentry doesn't work yet (build whould crash the api route).
|
|
||||||
excludeServerRoutes: [/\/api\/social\/og\/image\/?/],
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// Sentry should be the last thing to export to catch everything right
|
|
||||||
module.exports = process.env.NEXT_PUBLIC_SENTRY_DSN
|
|
||||||
? withSentryConfig(moduleExports, sentryWebpackPluginOptions)
|
|
||||||
: moduleExports;
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "2.7.12",
|
"version": "2.8.3",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -57,7 +57,6 @@
|
|||||||
"@radix-ui/react-switch": "^1.0.0",
|
"@radix-ui/react-switch": "^1.0.0",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.0",
|
"@radix-ui/react-toggle-group": "^1.0.0",
|
||||||
"@radix-ui/react-tooltip": "^1.0.0",
|
"@radix-ui/react-tooltip": "^1.0.0",
|
||||||
"@sentry/nextjs": "^7.20.0",
|
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^4.3.9",
|
||||||
|
|||||||
+16
-12
@@ -6,7 +6,9 @@ import { useEffect, useState } from "react";
|
|||||||
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { HeadSeo } from "@calcom/ui";
|
import { HeadSeo } from "@calcom/ui";
|
||||||
import { FiBookOpen, FiCheck, FiChevronRight, FiFileText } from "@calcom/ui/components/icon";
|
import { BookOpen, Check, ChevronRight, FileText } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
|
|
||||||
@@ -20,13 +22,13 @@ export default function Custom404() {
|
|||||||
{
|
{
|
||||||
title: t("documentation"),
|
title: t("documentation"),
|
||||||
description: t("documentation_description"),
|
description: t("documentation_description"),
|
||||||
icon: FiFileText,
|
icon: FileText,
|
||||||
href: DOCS_URL,
|
href: DOCS_URL,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("blog"),
|
title: t("blog"),
|
||||||
description: t("blog_description"),
|
description: t("blog_description"),
|
||||||
icon: FiBookOpen,
|
icon: BookOpen,
|
||||||
href: `${WEBSITE_URL}/blog`,
|
href: `${WEBSITE_URL}/blog`,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
@@ -112,7 +114,7 @@ export default function Custom404() {
|
|||||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||||
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -127,7 +129,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
|
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -140,7 +142,7 @@ export default function Custom404() {
|
|||||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
<span className="bg-muted flex h-12 w-12 items-center justify-center rounded-lg">
|
||||||
<FiFileText className="text-default h-6 w-6" aria-hidden="true" />
|
<FileText className="text-default h-6 w-6" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -153,7 +155,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
|
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -198,7 +200,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -257,7 +259,7 @@ export default function Custom404() {
|
|||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
<div className="flex-shrink-0">
|
<div className="flex-shrink-0">
|
||||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||||
<FiCheck className="h-6 w-6 text-green-500" aria-hidden="true" />
|
<Check className="h-6 w-6 text-green-500" aria-hidden="true" />
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="min-w-0 flex-1">
|
<div className="min-w-0 flex-1">
|
||||||
@@ -272,7 +274,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
|
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -300,7 +302,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{link.description}</p>
|
<p className="text-subtle text-base">{link.description}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
@@ -346,7 +348,7 @@ export default function Custom404() {
|
|||||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex-shrink-0 self-center">
|
<div className="flex-shrink-0 self-center">
|
||||||
<FiChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
<ChevronRight className="text-muted h-5 w-5" aria-hidden="true" />
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
</li>
|
</li>
|
||||||
@@ -366,6 +368,8 @@ export default function Custom404() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Custom404.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||||
const ssr = await ssgInit(context);
|
const ssr = await ssgInit(context);
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,9 @@ import { useRouter } from "next/router";
|
|||||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
import { FiCopy } from "@calcom/ui/components/icon";
|
import { Copy } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function Error500() {
|
export default function Error500() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -34,7 +36,7 @@ export default function Error500() {
|
|||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||||
StartIcon={FiCopy}
|
StartIcon={Copy}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(router.query.error as string);
|
navigator.clipboard.writeText(router.query.error as string);
|
||||||
showToast("Link copied!", "success");
|
showToast("Link copied!", "success");
|
||||||
@@ -52,3 +54,5 @@ export default function Error500() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Error500.PageWrapper = PageWrapper;
|
||||||
+14
-10
@@ -3,7 +3,6 @@ import classNames from "classnames";
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -24,16 +23,17 @@ import defaultEvents, {
|
|||||||
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";
|
||||||
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
import { Avatar, AvatarGroup, HeadSeo } from "@calcom/ui";
|
||||||
import { FiArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
export default function User(props: inferSSRProps<typeof getServerSideProps> & EmbedProps) {
|
||||||
@@ -60,7 +60,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<li
|
<li
|
||||||
key={index}
|
key={index}
|
||||||
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||||
<FiArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
<ArrowRight className="text-emphasis absolute right-3 top-3 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
<Link
|
<Link
|
||||||
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||||
className="flex justify-between px-6 py-4"
|
className="flex justify-between px-6 py-4"
|
||||||
@@ -93,14 +93,15 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
const query = { ...router.query };
|
const query = { ...router.query };
|
||||||
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
delete query.user; // So it doesn't display in the Link (and make tests fail)
|
||||||
const nameOrUsername = user.name || user.username || "";
|
const nameOrUsername = user.name || user.username || "";
|
||||||
const telemetry = useTelemetry();
|
|
||||||
|
|
||||||
useEffect(() => {
|
/*
|
||||||
|
const telemetry = useTelemetry();
|
||||||
|
useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/[user]"));
|
||||||
}
|
}
|
||||||
}, [telemetry, router.asPath]);
|
}, [telemetry, router.asPath]); */
|
||||||
const isEventListEmpty = eventTypes.length === 0;
|
const isEventListEmpty = eventTypes.length === 0;
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -122,7 +123,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<main
|
<main
|
||||||
className={classNames(
|
className={classNames(
|
||||||
shouldAlignCentrally ? "mx-auto" : "",
|
shouldAlignCentrally ? "mx-auto" : "",
|
||||||
isEmbed ? " border-bookinglightest bg-default rounded-md border" : "",
|
isEmbed ? " border-booker border-booker-width bg-default rounded-md border" : "",
|
||||||
"max-w-3xl py-24 px-4"
|
"max-w-3xl py-24 px-4"
|
||||||
)}>
|
)}>
|
||||||
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
||||||
@@ -161,7 +162,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
key={type.id}
|
key={type.id}
|
||||||
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
style={{ display: "flex", ...eventTypeListItemEmbedStyles }}
|
||||||
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
className=" border-subtle bg-default hover:bg-muted group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0">
|
||||||
<FiArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
<ArrowRight className="text-emphasis absolute right-4 top-4 h-4 w-4 opacity-0 transition-opacity group-hover:opacity-100" />
|
||||||
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
{/* Don't prefetch till the time we drop the amount of javascript in [user][type] page which is impacting score for [user] page */}
|
||||||
<div className="block w-full p-5">
|
<div className="block w-full p-5">
|
||||||
<Link
|
<Link
|
||||||
@@ -180,7 +181,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
<div className="flex flex-wrap items-center">
|
<div className="flex flex-wrap items-center">
|
||||||
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||||
</div>
|
</div>
|
||||||
<EventTypeDescription eventType={type} />
|
<EventTypeDescription eventType={type} isPublic={true} />
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -196,6 +197,9 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
User.isBookingPage = true;
|
||||||
|
User.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
const getEventTypesWithHiddenFromDB = async (userId: number) => {
|
||||||
return (
|
return (
|
||||||
await prisma.eventType.findMany({
|
await prisma.eventType.findMany({
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ import { isBrandingHidden } from "@lib/isBrandingHidden";
|
|||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
import AvailabilityPage from "@components/booking/pages/AvailabilityPage";
|
||||||
|
|
||||||
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
export type AvailabilityPageProps = inferSSRProps<typeof getStaticProps> & EmbedProps;
|
||||||
@@ -54,6 +55,9 @@ export default function Type(props: AvailabilityPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type.isBookingPage = true;
|
||||||
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
const paramsSchema = z.object({ type: z.string(), user: z.string() });
|
||||||
async function getUserPageProps(context: GetStaticPropsContext) {
|
async function getUserPageProps(context: GetStaticPropsContext) {
|
||||||
// load server side dependencies
|
// load server side dependencies
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import {
|
|||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -69,6 +70,9 @@ export default function Book(props: BookPageProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Book.isBookingPage = true;
|
||||||
|
Book.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
bookingUid: z.string().optional(),
|
bookingUid: z.string().optional(),
|
||||||
count: z.coerce.number().optional(),
|
count: z.coerce.number().optional(),
|
||||||
|
|||||||
+3
-72
@@ -1,82 +1,13 @@
|
|||||||
import { DefaultSeo } from "next-seo";
|
|
||||||
import { Inter } from "next/font/google";
|
|
||||||
import localFont from "next/font/local";
|
|
||||||
import Head from "next/head";
|
|
||||||
import Script from "next/script";
|
|
||||||
|
|
||||||
import "@calcom/embed-core/src/embed-iframe";
|
|
||||||
import LicenseRequired from "@calcom/features/ee/common/components/LicenseRequired";
|
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
import AppProviders from "@lib/app-providers";
|
|
||||||
import { seoConfig } from "@lib/config/next-seo.config";
|
|
||||||
|
|
||||||
import I18nLanguageHandler from "@components/I18nLanguageHandler";
|
|
||||||
|
|
||||||
import "../styles/globals.css";
|
import "../styles/globals.css";
|
||||||
|
|
||||||
const interFont = Inter({ subsets: ["latin"], variable: "--font-inter", preload: true, display: "swap" });
|
|
||||||
const calFont = localFont({
|
|
||||||
src: "../fonts/CalSans-SemiBold.woff2",
|
|
||||||
variable: "--font-cal",
|
|
||||||
preload: true,
|
|
||||||
display: "swap",
|
|
||||||
});
|
|
||||||
|
|
||||||
function MyApp(props: AppProps) {
|
function MyApp(props: AppProps) {
|
||||||
const { Component, pageProps, err, router } = props;
|
const { Component, pageProps } = props;
|
||||||
let pageStatus = "200";
|
if (Component.PageWrapper !== undefined) return Component.PageWrapper(props);
|
||||||
if (router.pathname === "/404") {
|
return <Component {...pageProps} />;
|
||||||
pageStatus = "404";
|
|
||||||
} else if (router.pathname === "/500") {
|
|
||||||
pageStatus = "500";
|
|
||||||
}
|
|
||||||
|
|
||||||
// On client side don't let nonce creep into DOM
|
|
||||||
// It also avoids hydration warning that says that Client has the nonce value but server has "" because browser removes nonce attributes before DOM is built
|
|
||||||
// See https://github.com/kentcdodds/nonce-hydration-issues
|
|
||||||
// Set "" only if server had it set otherwise keep it undefined because server has to match with client to avoid hydration error
|
|
||||||
const nonce = typeof window !== "undefined" ? (pageProps.nonce ? "" : undefined) : pageProps.nonce;
|
|
||||||
const providerProps = {
|
|
||||||
...props,
|
|
||||||
pageProps: {
|
|
||||||
...props.pageProps,
|
|
||||||
nonce,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
// Use the layout defined at the page level, if available
|
|
||||||
const getLayout = Component.getLayout ?? ((page) => page);
|
|
||||||
return (
|
|
||||||
<AppProviders {...providerProps}>
|
|
||||||
<DefaultSeo {...seoConfig.defaultNextSeo} />
|
|
||||||
<I18nLanguageHandler />
|
|
||||||
<Script
|
|
||||||
nonce={nonce}
|
|
||||||
id="page-status"
|
|
||||||
dangerouslySetInnerHTML={{ __html: `window.CalComPageStatus = '${pageStatus}'` }}
|
|
||||||
/>
|
|
||||||
<style jsx global>{`
|
|
||||||
:root {
|
|
||||||
--font-inter: ${interFont.style.fontFamily};
|
|
||||||
--font-cal: ${calFont.style.fontFamily};
|
|
||||||
}
|
|
||||||
`}</style>
|
|
||||||
<Head>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
|
||||||
</Head>
|
|
||||||
{getLayout(
|
|
||||||
Component.requiresLicense ? (
|
|
||||||
<LicenseRequired>
|
|
||||||
<Component {...pageProps} err={err} />
|
|
||||||
</LicenseRequired>
|
|
||||||
) : (
|
|
||||||
<Component {...pageProps} err={err} />
|
|
||||||
),
|
|
||||||
router
|
|
||||||
)}
|
|
||||||
</AppProviders>
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export default trpc.withTRPC(MyApp);
|
export default trpc.withTRPC(MyApp);
|
||||||
@@ -1,6 +1,5 @@
|
|||||||
import type { DocumentContext, DocumentProps } from "next/document";
|
import type { DocumentContext, DocumentProps } from "next/document";
|
||||||
import Document, { Head, Html, Main, NextScript } from "next/document";
|
import Document, { Head, Html, Main, NextScript } from "next/document";
|
||||||
import Script from "next/script";
|
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
|
|
||||||
import { getDirFromLang } from "@calcom/lib/i18n";
|
import { getDirFromLang } from "@calcom/lib/i18n";
|
||||||
@@ -42,7 +41,6 @@ class MyDocument extends Document<Props> {
|
|||||||
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
<link rel="mask-icon" href="/safari-pinned-tab.svg" color="#000000" />
|
||||||
<meta name="msapplication-TileColor" content="#ff0000" />
|
<meta name="msapplication-TileColor" content="#ff0000" />
|
||||||
<meta name="theme-color" content="var(--cal-bg)" />
|
<meta name="theme-color" content="var(--cal-bg)" />
|
||||||
<Script src="/embed-init-iframe.js" strategy="beforeInteractive" />
|
|
||||||
</Head>
|
</Head>
|
||||||
|
|
||||||
<body
|
<body
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
* Typescript class based component for custom-error
|
* Typescript class based component for custom-error
|
||||||
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
* @link https://nextjs.org/docs/advanced-features/custom-error-page
|
||||||
*/
|
*/
|
||||||
import * as Sentry from "@sentry/nextjs";
|
|
||||||
import type { NextPage, NextPageContext } from "next";
|
import type { NextPage, NextPageContext } from "next";
|
||||||
import type { ErrorProps } from "next/error";
|
import type { ErrorProps } from "next/error";
|
||||||
import NextError from "next/error";
|
import NextError from "next/error";
|
||||||
@@ -50,7 +49,6 @@ const CustomError: NextPage<CustomErrorProps> = (props) => {
|
|||||||
*/
|
*/
|
||||||
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
CustomError.getInitialProps = async (ctx: AugmentedNextPageContext) => {
|
||||||
const { res, err, asPath } = ctx;
|
const { res, err, asPath } = ctx;
|
||||||
await Sentry.captureUnderscoreErrorException(ctx);
|
|
||||||
const errorInitialProps = (await NextError.getInitialProps({
|
const errorInitialProps = (await NextError.getInitialProps({
|
||||||
res,
|
res,
|
||||||
err,
|
err,
|
||||||
|
|||||||
@@ -1,29 +0,0 @@
|
|||||||
import type { NextApiRequest } from "next";
|
|
||||||
import { z } from "zod";
|
|
||||||
|
|
||||||
import { getUserAvailability } from "@calcom/core/getUserAvailability";
|
|
||||||
import { defaultResponder } from "@calcom/lib/server";
|
|
||||||
import { stringOrNumber } from "@calcom/prisma/zod-utils";
|
|
||||||
|
|
||||||
const availabilitySchema = z.object({
|
|
||||||
user: z.string(),
|
|
||||||
dateFrom: z.string(),
|
|
||||||
dateTo: z.string(),
|
|
||||||
eventTypeId: stringOrNumber.optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @deprecated Use TRCP's viewer.availability.user
|
|
||||||
*/
|
|
||||||
|
|
||||||
async function handler(req: NextApiRequest) {
|
|
||||||
const { user: username, eventTypeId, dateTo, dateFrom } = availabilitySchema.parse(req.query);
|
|
||||||
return getUserAvailability({
|
|
||||||
username,
|
|
||||||
dateFrom,
|
|
||||||
dateTo,
|
|
||||||
eventTypeId,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export default defaultResponder(handler);
|
|
||||||
@@ -0,0 +1,147 @@
|
|||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { z } from "zod";
|
||||||
|
|
||||||
|
import { DailyLocationType } from "@calcom/app-store/locations";
|
||||||
|
import { getDownloadLinkOfCalVideoByRecordingId } from "@calcom/core/videoClient";
|
||||||
|
import { sendDailyVideoRecordingEmails } from "@calcom/emails";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||||
|
import { defaultHandler } from "@calcom/lib/server";
|
||||||
|
import { getTranslation } from "@calcom/lib/server/i18n";
|
||||||
|
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||||
|
import type { CalendarEvent } from "@calcom/types/Calendar";
|
||||||
|
|
||||||
|
const schema = z.object({
|
||||||
|
recordingId: z.string(),
|
||||||
|
bookingUID: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const downloadLinkSchema = z.object({
|
||||||
|
download_link: z.string(),
|
||||||
|
});
|
||||||
|
|
||||||
|
async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
|
if (!process.env.SENDGRID_API_KEY || !process.env.SENDGRID_EMAIL) {
|
||||||
|
return res.status(405).json({ message: "No SendGrid API key or email" });
|
||||||
|
}
|
||||||
|
const response = schema.safeParse(JSON.parse(req.body));
|
||||||
|
|
||||||
|
if (!response.success) {
|
||||||
|
return res.status(400).send({
|
||||||
|
message: "Invalid Payload",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const { recordingId, bookingUID } = response.data;
|
||||||
|
const session = await getServerSession({ req, res });
|
||||||
|
|
||||||
|
if (!session?.user) {
|
||||||
|
return res.status(401).send({
|
||||||
|
message: "User not logged in",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const booking = await prisma.booking.findFirst({
|
||||||
|
where: {
|
||||||
|
uid: bookingUID,
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
...bookingMinimalSelect,
|
||||||
|
uid: true,
|
||||||
|
location: true,
|
||||||
|
isRecorded: true,
|
||||||
|
user: {
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
credentials: true,
|
||||||
|
timeZone: true,
|
||||||
|
email: true,
|
||||||
|
name: true,
|
||||||
|
locale: true,
|
||||||
|
destinationCalendar: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!booking || booking.location !== DailyLocationType) {
|
||||||
|
return res.status(404).send({
|
||||||
|
message: `Booking of uid ${bookingUID} does not exist or does not contain daily video as location`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const t = await getTranslation(booking?.user?.locale ?? "en", "common");
|
||||||
|
const attendeesListPromises = booking.attendees.map(async (attendee) => {
|
||||||
|
return {
|
||||||
|
id: attendee.id,
|
||||||
|
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 isUserAttendeeOrOrganiser =
|
||||||
|
booking?.user?.id === session.user.id ||
|
||||||
|
attendeesList.find((attendee) => attendee.id === session.user.id);
|
||||||
|
|
||||||
|
if (!isUserAttendeeOrOrganiser) {
|
||||||
|
return res.status(403).send({
|
||||||
|
message: "Unauthorised",
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
await prisma.booking.update({
|
||||||
|
where: {
|
||||||
|
uid: booking.uid,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
isRecorded: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const isSendingEmailsAllowed = IS_SELF_HOSTED || session?.user?.belongsToActiveTeam;
|
||||||
|
|
||||||
|
// send emails to all attendees only when user has team plan
|
||||||
|
if (isSendingEmailsAllowed) {
|
||||||
|
const response = await getDownloadLinkOfCalVideoByRecordingId(recordingId);
|
||||||
|
|
||||||
|
const downloadLinkResponse = downloadLinkSchema.parse(response);
|
||||||
|
const downloadLink = downloadLinkResponse.download_link;
|
||||||
|
|
||||||
|
const evt: CalendarEvent = {
|
||||||
|
type: booking.title,
|
||||||
|
title: booking.title,
|
||||||
|
description: booking.description || undefined,
|
||||||
|
startTime: booking.startTime.toISOString(),
|
||||||
|
endTime: booking.endTime.toISOString(),
|
||||||
|
organizer: {
|
||||||
|
email: booking.user?.email || "Email-less",
|
||||||
|
name: booking.user?.name || "Nameless",
|
||||||
|
timeZone: booking.user?.timeZone || "Europe/London",
|
||||||
|
language: { translate: t, locale: booking?.user?.locale ?? "en" },
|
||||||
|
},
|
||||||
|
attendees: attendeesList,
|
||||||
|
uid: booking.uid,
|
||||||
|
};
|
||||||
|
|
||||||
|
await sendDailyVideoRecordingEmails(evt, downloadLink);
|
||||||
|
return res.status(200).json({ message: "Success" });
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.status(403).json({ message: "User does not have team plan to send out emails" });
|
||||||
|
} catch (err) {
|
||||||
|
console.warn("something_went_wrong", err);
|
||||||
|
return res.status(500).json({ message: "something went wrong" });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export default defaultHandler({
|
||||||
|
POST: Promise.resolve({ default: handler }),
|
||||||
|
});
|
||||||
@@ -9,13 +9,16 @@ import type { AppGetServerSideProps } from "@calcom/types/AppGetServerSideProps"
|
|||||||
|
|
||||||
import type { AppProps } from "@lib/app-providers";
|
import type { AppProps } from "@lib/app-providers";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
type AppPageType = {
|
type AppPageType = {
|
||||||
getServerSideProps: AppGetServerSideProps;
|
getServerSideProps: AppGetServerSideProps;
|
||||||
// A component than can accept any properties
|
// A component than can accept any properties
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
default: ((props: any) => JSX.Element) & Pick<AppProps["Component"], "isThemeSupported" | "getLayout">;
|
default: ((props: any) => JSX.Element) &
|
||||||
|
Pick<AppProps["Component"], "isBookingPage" | "getLayout" | "PageWrapper">;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Found = {
|
type Found = {
|
||||||
@@ -70,17 +73,17 @@ const AppPage: AppPageType["default"] = function AppPage(props) {
|
|||||||
return <route.Component {...componentProps} />;
|
return <route.Component {...componentProps} />;
|
||||||
};
|
};
|
||||||
|
|
||||||
AppPage.isThemeSupported = ({ router }) => {
|
AppPage.isBookingPage = ({ router }) => {
|
||||||
const route = getRoute(router.query.slug as string, router.query.pages as string[]);
|
const route = getRoute(router.query.slug as string, router.query.pages as string[]);
|
||||||
if (route.notFound) {
|
if (route.notFound) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
const isThemeSupported = route.Component.isThemeSupported;
|
const isBookingPage = route.Component.isBookingPage;
|
||||||
if (typeof isThemeSupported === "function") {
|
if (typeof isBookingPage === "function") {
|
||||||
return isThemeSupported({ router });
|
return isBookingPage({ router });
|
||||||
}
|
}
|
||||||
|
|
||||||
return !!isThemeSupported;
|
return !!isBookingPage;
|
||||||
};
|
};
|
||||||
|
|
||||||
AppPage.getLayout = (page, router) => {
|
AppPage.getLayout = (page, router) => {
|
||||||
@@ -94,6 +97,8 @@ AppPage.getLayout = (page, router) => {
|
|||||||
return route.Component.getLayout(page, router);
|
return route.Component.getLayout(page, router);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AppPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default AppPage;
|
export default AppPage;
|
||||||
|
|
||||||
export async function getServerSideProps(
|
export async function getServerSideProps(
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import prisma from "@calcom/prisma";
|
|||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import App from "@components/apps/App";
|
import App from "@components/apps/App";
|
||||||
|
|
||||||
const md = new MarkdownIt("default", { html: true, breaks: true });
|
const md = new MarkdownIt("default", { html: true, breaks: true });
|
||||||
@@ -122,4 +123,6 @@ export const getStaticProps = async (ctx: GetStaticPropsContext) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
SingleAppPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default SingleAppPage;
|
export default SingleAppPage;
|
||||||
@@ -5,6 +5,8 @@ import { useRouter } from "next/router";
|
|||||||
import { AppSetupPage } from "@calcom/app-store/_pages/setup";
|
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 PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function SetupInformation(props: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const slug = router.query.slug as string;
|
const slug = router.query.slug as string;
|
||||||
@@ -26,6 +28,8 @@ export default function SetupInformation(props: InferGetStaticPropsType<typeof g
|
|||||||
return <AppSetupPage slug={slug} {...props} />;
|
return <AppSetupPage slug={slug} {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
SetupInformation.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticPaths: GetStaticPaths = async () => {
|
export const getStaticPaths: GetStaticPaths = async () => {
|
||||||
return {
|
return {
|
||||||
paths: [],
|
paths: [],
|
||||||
|
|||||||
@@ -9,6 +9,8 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { AppCard, SkeletonText } from "@calcom/ui";
|
import { AppCard, SkeletonText } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -47,6 +49,8 @@ export default function Apps({ apps }: InferGetStaticPropsType<typeof getStaticP
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Apps.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticPaths = async () => {
|
export const getStaticPaths = async () => {
|
||||||
const paths = Object.keys(AppCategories);
|
const paths = Object.keys(AppCategories);
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,9 @@ import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
|||||||
import Shell from "@calcom/features/shell/Shell";
|
import Shell from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { SkeletonText } from "@calcom/ui";
|
import { SkeletonText } from "@calcom/ui";
|
||||||
import { FiArrowLeft, FiArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowLeft, ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
@@ -16,7 +18,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
<Link
|
<Link
|
||||||
href="/apps"
|
href="/apps"
|
||||||
className="text-emphasis inline-flex items-center justify-start gap-1 rounded-sm py-2">
|
className="text-emphasis inline-flex items-center justify-start gap-1 rounded-sm py-2">
|
||||||
<FiArrowLeft className="h-4 w-4" />
|
<ArrowLeft className="h-4 w-4" />
|
||||||
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
{isLocaleReady ? t("app_store") : <SkeletonText className="h-6 w-24" />}{" "}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
@@ -32,7 +34,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||||
<p className="text-subtle text-sm">
|
<p className="text-subtle text-sm">
|
||||||
{t("number_apps", { count: category.count })}{" "}
|
{t("number_apps", { count: category.count })}{" "}
|
||||||
<FiArrowRight className="inline-block h-4 w-4" />
|
<ArrowRight className="inline-block h-4 w-4" />
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -43,6 +45,8 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Apps.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticProps = async () => {
|
export const getStaticProps = async () => {
|
||||||
const appStore = await getAppRegistry();
|
const appStore = await getAppRegistry();
|
||||||
const categories = appStore.reduce((c, app) => {
|
const categories = appStore.reduce((c, app) => {
|
||||||
|
|||||||
@@ -10,8 +10,9 @@ import type { AppCategories } from "@calcom/prisma/client";
|
|||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import type { HorizontalTabItemProps } from "@calcom/ui";
|
import type { HorizontalTabItemProps } from "@calcom/ui";
|
||||||
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
|
import { AllApps, AppStoreCategories, HorizontalTabs, TextField, PopularAppsSlider } from "@calcom/ui";
|
||||||
import { FiSearch } from "@calcom/ui/components/icon";
|
import { Search } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
import AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -37,7 +38,7 @@ function AppsSearch({
|
|||||||
return (
|
return (
|
||||||
<TextField
|
<TextField
|
||||||
className="bg-subtle !border-muted !pl-0 focus:!ring-offset-0"
|
className="bg-subtle !border-muted !pl-0 focus:!ring-offset-0"
|
||||||
addOnLeading={<FiSearch className="text-subtle h-4 w-4" />}
|
addOnLeading={<Search className="text-subtle h-4 w-4" />}
|
||||||
addOnClassname="!border-muted"
|
addOnClassname="!border-muted"
|
||||||
containerClassName={classNames("focus:!ring-offset-0", className)}
|
containerClassName={classNames("focus:!ring-offset-0", className)}
|
||||||
type="search"
|
type="search"
|
||||||
@@ -85,6 +86,8 @@ export default function Apps({ categories, appStore }: inferSSRProps<typeof getS
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Apps.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
||||||
|
|||||||
@@ -30,20 +30,21 @@ import {
|
|||||||
showToast,
|
showToast,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiBarChart,
|
BarChart,
|
||||||
FiCalendar,
|
Calendar,
|
||||||
FiCreditCard,
|
CreditCard,
|
||||||
FiGrid,
|
Grid,
|
||||||
FiMoreHorizontal,
|
MoreHorizontal,
|
||||||
FiPlus,
|
Plus,
|
||||||
FiShare2,
|
Share2,
|
||||||
FiTrash,
|
Trash,
|
||||||
FiVideo,
|
Video,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import AppListCard from "@components/AppListCard";
|
import AppListCard from "@components/AppListCard";
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
import { CalendarListContainer } from "@components/apps/CalendarListContainer";
|
||||||
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
import InstalledAppsLayout from "@components/apps/layouts/InstalledAppsLayout";
|
||||||
|
|
||||||
@@ -71,7 +72,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
|||||||
color="destructive"
|
color="destructive"
|
||||||
onClick={() => handleDisconnect(credentialId)}
|
onClick={() => handleDisconnect(credentialId)}
|
||||||
disabled={isGlobal}
|
disabled={isGlobal}
|
||||||
StartIcon={FiTrash}>
|
StartIcon={Trash}>
|
||||||
{t("remove_app")}
|
{t("remove_app")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
@@ -159,7 +160,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
|||||||
<div className="flex justify-end">
|
<div className="flex justify-end">
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
{!appIsDefault && variant === "conferencing" && (
|
{!appIsDefault && variant === "conferencing" && (
|
||||||
@@ -167,7 +168,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
type="button"
|
type="button"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiVideo}
|
StartIcon={Video}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
const locationType = getEventLocationTypeFromApp(
|
const locationType = getEventLocationTypeFromApp(
|
||||||
item?.locationOption?.value ?? ""
|
item?.locationOption?.value ?? ""
|
||||||
@@ -225,13 +226,13 @@ const IntegrationsContainer = ({
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
||||||
const emptyIcon = {
|
const emptyIcon = {
|
||||||
calendar: FiCalendar,
|
calendar: Calendar,
|
||||||
conferencing: FiVideo,
|
conferencing: Video,
|
||||||
automation: FiShare2,
|
automation: Share2,
|
||||||
analytics: FiBarChart,
|
analytics: BarChart,
|
||||||
payment: FiCreditCard,
|
payment: CreditCard,
|
||||||
web3: FiBarChart,
|
web3: BarChart,
|
||||||
other: FiGrid,
|
other: Grid,
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -270,7 +271,7 @@ const IntegrationsContainer = ({
|
|||||||
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
|
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
|
||||||
}
|
}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiPlus}>
|
StartIcon={Plus}>
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -372,3 +373,5 @@ export async function getServerSideProps(ctx: AppGetServerSidePropsContext) {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InstalledApps.PageWrapper = PageWrapper;
|
||||||
@@ -5,8 +5,9 @@ import z from "zod";
|
|||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, SkeletonText } from "@calcom/ui";
|
import { Button, SkeletonText } from "@calcom/ui";
|
||||||
import { FiX } from "@calcom/ui/components/icon";
|
import { X } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
|
||||||
import { ssgInit } from "@server/lib/ssg";
|
import { ssgInit } from "@server/lib/ssg";
|
||||||
@@ -29,7 +30,7 @@ export default function Error() {
|
|||||||
<AuthContainer title="" description="">
|
<AuthContainer title="" description="">
|
||||||
<div>
|
<div>
|
||||||
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
<div className="bg-error mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<FiX className="h-6 w-6 text-red-600" />
|
<X className="h-6 w-6 text-red-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
@@ -49,6 +50,8 @@ export default function Error() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Error.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
export const getStaticProps = async (context: GetStaticPropsContext) => {
|
||||||
const ssr = await ssgInit(context);
|
const ssr = await ssgInit(context);
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import type { GetServerSidePropsContext } from "next";
|
|||||||
import { getCsrfToken } from "next-auth/react";
|
import { getCsrfToken } from "next-auth/react";
|
||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
|
import type { CSSProperties } from "react";
|
||||||
import React, { useMemo } from "react";
|
import React, { useMemo } from "react";
|
||||||
|
|
||||||
import dayjs from "@calcom/dayjs";
|
import dayjs from "@calcom/dayjs";
|
||||||
@@ -11,6 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { Button, TextField } from "@calcom/ui";
|
import { Button, TextField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
@@ -109,6 +111,14 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
|||||||
<>
|
<>
|
||||||
<form
|
<form
|
||||||
className="space-y-6"
|
className="space-y-6"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--cal-brand": "#111827",
|
||||||
|
"--cal-brand-emphasis": "#101010",
|
||||||
|
"--cal-brand-text": "white",
|
||||||
|
"--cal-brand-subtle": "#9CA3AF",
|
||||||
|
} as CSSProperties
|
||||||
|
}
|
||||||
onSubmit={async (e) => {
|
onSubmit={async (e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
|
|
||||||
@@ -164,6 +174,8 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Page.isThemeSupported = false;
|
||||||
|
Page.PageWrapper = PageWrapper;
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const id = context.params?.id as string;
|
const id = context.params?.id as string;
|
||||||
|
|
||||||
|
|||||||
@@ -4,13 +4,14 @@ import { getCsrfToken } from "next-auth/react";
|
|||||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { SyntheticEvent } from "react";
|
import type { CSSProperties, SyntheticEvent } from "react";
|
||||||
import React from "react";
|
import React from "react";
|
||||||
|
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button, EmailField } from "@calcom/ui";
|
import { Button, EmailField } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
|
||||||
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||||
@@ -105,7 +106,18 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
|||||||
{!success && (
|
{!success && (
|
||||||
<>
|
<>
|
||||||
<div className="space-y-6">{error && <p className="text-red-600">{error.message}</p>}</div>
|
<div className="space-y-6">{error && <p className="text-red-600">{error.message}</p>}</div>
|
||||||
<form className="space-y-6" onSubmit={handleSubmit} action="#">
|
<form
|
||||||
|
className="space-y-6"
|
||||||
|
onSubmit={handleSubmit}
|
||||||
|
action="#"
|
||||||
|
style={
|
||||||
|
{
|
||||||
|
"--cal-brand": "#111827",
|
||||||
|
"--cal-brand-emphasis": "#101010",
|
||||||
|
"--cal-brand-text": "white",
|
||||||
|
"--cal-brand-subtle": "#9CA3AF",
|
||||||
|
} as CSSProperties
|
||||||
|
}>
|
||||||
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
<input name="csrfToken" type="hidden" defaultValue={csrfToken} hidden />
|
||||||
<EmailField
|
<EmailField
|
||||||
onChange={handleChange}
|
onChange={handleChange}
|
||||||
@@ -132,6 +144,9 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ForgotPassword.isThemeSupported = false;
|
||||||
|
ForgotPassword.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
const { req, res } = context;
|
const { req, res } = context;
|
||||||
|
|
||||||
|
|||||||
@@ -19,13 +19,14 @@ 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 { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
import { Alert, Button, EmailField, PasswordField } from "@calcom/ui";
|
||||||
import { FiArrowLeft } from "@calcom/ui/components/icon";
|
import { ArrowLeft } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { WithNonceProps } from "@lib/withNonce";
|
import type { WithNonceProps } from "@lib/withNonce";
|
||||||
import withNonce from "@lib/withNonce";
|
import withNonce from "@lib/withNonce";
|
||||||
|
|
||||||
import AddToHomescreen from "@components/AddToHomescreen";
|
import AddToHomescreen from "@components/AddToHomescreen";
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import TwoFactor from "@components/auth/TwoFactor";
|
import TwoFactor from "@components/auth/TwoFactor";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
|
||||||
@@ -91,7 +92,7 @@ export default function Login({
|
|||||||
setTwoFactorRequired(false);
|
setTwoFactorRequired(false);
|
||||||
methods.setValue("totpCode", "");
|
methods.setValue("totpCode", "");
|
||||||
}}
|
}}
|
||||||
StartIcon={FiArrowLeft}
|
StartIcon={ArrowLeft}
|
||||||
color="minimal">
|
color="minimal">
|
||||||
{t("go_back")}
|
{t("go_back")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -302,5 +303,6 @@ const _getServerSideProps = async function getServerSideProps(context: GetServer
|
|||||||
};
|
};
|
||||||
|
|
||||||
Login.isThemeSupported = false;
|
Login.isThemeSupported = false;
|
||||||
|
Login.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getServerSideProps = withNonce(_getServerSideProps);
|
export const getServerSideProps = withNonce(_getServerSideProps);
|
||||||
@@ -6,10 +6,11 @@ import { useEffect } from "react";
|
|||||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
import { FiCheck } from "@calcom/ui/components/icon";
|
import { Check } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import AuthContainer from "@components/ui/AuthContainer";
|
import AuthContainer from "@components/ui/AuthContainer";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -32,7 +33,7 @@ export function Logout(props: Props) {
|
|||||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
<div className="bg-success mx-auto flex h-12 w-12 items-center justify-center rounded-full">
|
||||||
<FiCheck className="h-6 w-6 text-green-600" />
|
<Check className="h-6 w-6 text-green-600" />
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-3 text-center sm:mt-5">
|
<div className="mt-3 text-center sm:mt-5">
|
||||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||||
@@ -51,7 +52,7 @@ export function Logout(props: Props) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Logout.isThemeSupported = false;
|
Logout.isThemeSupported = false;
|
||||||
|
Logout.PageWrapper = PageWrapper;
|
||||||
export default Logout;
|
export default Logout;
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|||||||
@@ -2,6 +2,8 @@ import { signIn } from "next-auth/react";
|
|||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
// To handle the IdP initiated login flow callback
|
// To handle the IdP initiated login flow callback
|
||||||
export default function Page() {
|
export default function Page() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -21,3 +23,4 @@ export default function Page() {
|
|||||||
|
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
Page.PageWrapper = PageWrapper;
|
||||||
@@ -11,6 +11,7 @@ import prisma from "@calcom/prisma";
|
|||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { Meta, WizardForm } from "@calcom/ui";
|
import { Meta, WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
||||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||||
@@ -136,7 +137,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
Setup.isThemeSupported = false;
|
Setup.isThemeSupported = false;
|
||||||
|
Setup.PageWrapper = PageWrapper;
|
||||||
export default Setup;
|
export default Setup;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { getProviders, signIn, getCsrfToken } from "next-auth/react";
|
|||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
type Provider = {
|
type Provider = {
|
||||||
name: string;
|
name: string;
|
||||||
id: string;
|
id: string;
|
||||||
@@ -23,6 +25,8 @@ function signin({ providers }: { providers: Provider[] }) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
signin.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default signin;
|
export default signin;
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
|||||||
@@ -19,6 +19,8 @@ import prisma from "@calcom/prisma";
|
|||||||
import { asStringOrNull } from "@lib/asStringOrNull";
|
import { asStringOrNull } from "@lib/asStringOrNull";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
|
export type SSOProviderPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||||
@@ -49,6 +51,8 @@ export default function Provider(props: SSOProviderPageProps) {
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Provider.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||||
// get query params and typecast them to string
|
// get query params and typecast them to string
|
||||||
// (would be even better to assert them instead of typecasting)
|
// (would be even better to assert them instead of typecasting)
|
||||||
|
|||||||
@@ -6,6 +6,8 @@ import { HOSTED_CAL_FEATURES } from "@calcom/lib/constants";
|
|||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
|
// This page is used to initiate the SAML authentication flow by redirecting to the SAML provider.
|
||||||
// Accessible only on self-hosted Cal.com instances.
|
// Accessible only on self-hosted Cal.com instances.
|
||||||
export default function Page({ samlTenantID, samlProductID }: inferSSRProps<typeof getServerSideProps>) {
|
export default function Page({ samlTenantID, samlProductID }: inferSSRProps<typeof getServerSideProps>) {
|
||||||
@@ -28,6 +30,8 @@ export default function Page({ samlTenantID, samlProductID }: inferSSRProps<type
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Page.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export async function getServerSideProps() {
|
export async function getServerSideProps() {
|
||||||
return {
|
return {
|
||||||
props: {
|
props: {
|
||||||
|
|||||||
@@ -11,6 +11,7 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import { Button, showToast } from "@calcom/ui";
|
import { Button, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import Loader from "@components/Loader";
|
import Loader from "@components/Loader";
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
async function sendVerificationLogin(email: string, username: string) {
|
async function sendVerificationLogin(email: string, username: string) {
|
||||||
await signIn("email", {
|
await signIn("email", {
|
||||||
@@ -177,3 +178,5 @@ export default function Verify() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Verify.PageWrapper = PageWrapper;
|
||||||
@@ -27,10 +27,11 @@ import {
|
|||||||
ConfirmationDialogContent,
|
ConfirmationDialogContent,
|
||||||
VerticalDivider,
|
VerticalDivider,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { FiInfo, FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
import { Info, Plus, Trash } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||||
import EditableHeading from "@components/ui/EditableHeading";
|
import EditableHeading from "@components/ui/EditableHeading";
|
||||||
|
|
||||||
@@ -57,7 +58,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
|||||||
{t("date_overrides")}{" "}
|
{t("date_overrides")}{" "}
|
||||||
<Tooltip content={t("date_overrides_info")}>
|
<Tooltip content={t("date_overrides_info")}>
|
||||||
<span className="inline-block">
|
<span className="inline-block">
|
||||||
<FiInfo />
|
<Info className="h-4 w-4" />
|
||||||
</span>
|
</span>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</h3>
|
</h3>
|
||||||
@@ -75,7 +76,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
|||||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||||
onChange={(ranges) => append({ ranges })}
|
onChange={(ranges) => append({ ranges })}
|
||||||
Trigger={
|
Trigger={
|
||||||
<Button color="secondary" StartIcon={FiPlus} data-testid="add-override">
|
<Button color="secondary" StartIcon={Plus} data-testid="add-override">
|
||||||
Add an override
|
Add an override
|
||||||
</Button>
|
</Button>
|
||||||
}
|
}
|
||||||
@@ -202,7 +203,7 @@ export default function Availability() {
|
|||||||
<Dialog>
|
<Dialog>
|
||||||
<DialogTrigger asChild>
|
<DialogTrigger asChild>
|
||||||
<Button
|
<Button
|
||||||
StartIcon={FiTrash}
|
StartIcon={Trash}
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="destructive"
|
color="destructive"
|
||||||
aria-label={t("delete")}
|
aria-label={t("delete")}
|
||||||
@@ -301,3 +302,5 @@ export default function Availability() {
|
|||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Availability.PageWrapper = PageWrapper;
|
||||||
@@ -6,11 +6,12 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { EmptyScreen, showToast } from "@calcom/ui";
|
import { EmptyScreen, showToast } from "@calcom/ui";
|
||||||
import { FiClock } from "@calcom/ui/components/icon";
|
import { Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||||
|
|
||||||
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availability"]["list"]) {
|
||||||
@@ -75,7 +76,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
{schedules.length === 0 ? (
|
{schedules.length === 0 ? (
|
||||||
<div className="flex justify-center">
|
<div className="flex justify-center">
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiClock}
|
Icon={Clock}
|
||||||
headline={t("new_schedule_heading")}
|
headline={t("new_schedule_heading")}
|
||||||
description={t("new_schedule_description")}
|
description={t("new_schedule_description")}
|
||||||
buttonRaw={<NewScheduleButton />}
|
buttonRaw={<NewScheduleButton />}
|
||||||
@@ -116,3 +117,5 @@ export default function AvailabilityPage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
AvailabilityPage.PageWrapper = PageWrapper;
|
||||||
@@ -7,6 +7,8 @@ import { SkeletonText } from "@calcom/ui";
|
|||||||
|
|
||||||
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
import useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
type User = RouterOutputs["viewer"]["me"];
|
type User = RouterOutputs["viewer"]["me"];
|
||||||
|
|
||||||
export interface IBusySlot {
|
export interface IBusySlot {
|
||||||
@@ -126,6 +128,7 @@ export default function Troubleshoot() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
Troubleshoot.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
function convertMinsToHrsMins(mins: number) {
|
function convertMinsToHrsMins(mins: number) {
|
||||||
const h = Math.floor(mins / 60);
|
const h = Math.floor(mins / 60);
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ import {
|
|||||||
useIsBackgroundTransparent,
|
useIsBackgroundTransparent,
|
||||||
useIsEmbed,
|
useIsEmbed,
|
||||||
} from "@calcom/embed-core/embed-iframe";
|
} from "@calcom/embed-core/embed-iframe";
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
import {
|
import {
|
||||||
SystemField,
|
SystemField,
|
||||||
getBookingFieldsWithSystemFields,
|
getBookingFieldsWithSystemFields,
|
||||||
@@ -41,7 +42,6 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||||
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
import { maybeGetBookingUidFromSeat } from "@calcom/lib/server/maybeGetBookingUidFromSeat";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
|
||||||
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
import { getIs24hClockFromLocalStorage, isBrowserLocale24h } from "@calcom/lib/timeFormat";
|
||||||
import { localStorage } from "@calcom/lib/webstorage";
|
import { localStorage } from "@calcom/lib/webstorage";
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
@@ -49,11 +49,12 @@ import type { Prisma } from "@calcom/prisma/client";
|
|||||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme } from "@calcom/ui";
|
import { Button, EmailInput, HeadSeo, Badge, useCalcomTheme } from "@calcom/ui";
|
||||||
import { FiX, FiExternalLink, FiChevronLeft, FiCheck, FiCalendar } from "@calcom/ui/components/icon";
|
import { X, ExternalLink, ChevronLeft, Check, Calendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { timeZone } from "@lib/clock";
|
import { timeZone } from "@lib/clock";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import CancelBooking from "@components/booking/CancelBooking";
|
import CancelBooking from "@components/booking/CancelBooking";
|
||||||
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
import EventReservationSchema from "@components/schemas/EventReservationSchema";
|
||||||
|
|
||||||
@@ -106,10 +107,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
seatReferenceUid,
|
seatReferenceUid,
|
||||||
} = querySchema.parse(router.query);
|
} = querySchema.parse(router.query);
|
||||||
|
|
||||||
const tz =
|
const attendeeTimeZone = props?.bookingInfo?.attendees.find(
|
||||||
(isSuccessBookingPage
|
(attendee) => attendee.email === email
|
||||||
? props.bookingInfo.attendees.find((attendee) => attendee.email === email)?.timeZone
|
)?.timeZone;
|
||||||
: props.bookingInfo.eventType?.timeZone || props.bookingInfo.user?.timeZone) || timeZone();
|
const tz = isSuccessBookingPage && attendeeTimeZone ? attendeeTimeZone : props.tz ? props.tz : timeZone();
|
||||||
|
|
||||||
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
const location = props.bookingInfo.location as ReturnType<typeof getEventLocationValue>;
|
||||||
|
|
||||||
@@ -177,7 +178,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
// - Event Type has require confirmation option enabled always
|
// - Event Type has require confirmation option enabled always
|
||||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||||
// - It's a paid event and payment is pending.
|
// - It's a paid event and payment is pending.
|
||||||
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING;
|
const needsConfirmation = bookingInfo.status === BookingStatus.PENDING && eventType.requiresConfirmation;
|
||||||
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
const userIsOwner = !!(session?.user?.id && eventType.owner?.id === session.user.id);
|
||||||
|
|
||||||
const isCancelled =
|
const isCancelled =
|
||||||
@@ -186,13 +187,13 @@ export default function Success(props: SuccessProps) {
|
|||||||
(!!seatReferenceUid &&
|
(!!seatReferenceUid &&
|
||||||
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
!bookingInfo.seatsReferences.some((reference) => reference.referenceUid === seatReferenceUid));
|
||||||
|
|
||||||
const telemetry = useTelemetry();
|
// const telemetry = useTelemetry();
|
||||||
useEffect(() => {
|
/* useEffect(() => {
|
||||||
if (top !== window) {
|
if (top !== window) {
|
||||||
//page_view will be collected automatically by _middleware.ts
|
//page_view will be collected automatically by _middleware.ts
|
||||||
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
|
telemetry.event(telemetryEventTypes.embedView, collectPageParameters("/booking"));
|
||||||
}
|
}
|
||||||
}, [telemetry]);
|
}, [telemetry]); */
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const users = eventType.users;
|
const users = eventType.users;
|
||||||
@@ -269,7 +270,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
return t("emailed_you_and_attendees" + titleSuffix);
|
return t("emailed_you_and_attendees" + titleSuffix);
|
||||||
}
|
}
|
||||||
|
|
||||||
useTheme(isSuccessBookingPage ? props.profile.theme : "light");
|
// This is a weird case where the same route can be opened in booking flow as a success page or as a booking detail page from the app
|
||||||
|
// As Booking Page it has to support configured theme, but as booking detail page it should not do any change. Let Shell.tsx handle it.
|
||||||
|
useTheme(isSuccessBookingPage ? props.profile.theme : undefined);
|
||||||
useBrandColors({
|
useBrandColors({
|
||||||
brandColor: props.profile.brandColor,
|
brandColor: props.profile.brandColor,
|
||||||
darkBrandColor: props.profile.darkBrandColor,
|
darkBrandColor: props.profile.darkBrandColor,
|
||||||
@@ -306,7 +309,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
<Link
|
<Link
|
||||||
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
|
href={allRemainingBookings ? "/bookings/recurring" : "/bookings/upcoming"}
|
||||||
className="hover:bg-subtle text-subtle dark:hover:text-inverted hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
|
className="hover:bg-subtle text-subtle dark:hover:text-inverted hover:text-default mt-2 inline-flex px-1 py-2 text-sm dark:hover:bg-transparent">
|
||||||
<FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
<ChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -325,7 +328,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
"main inline-block transform overflow-hidden rounded-lg border sm:my-8 sm:max-w-xl",
|
||||||
isBackgroundTransparent ? "" : " bg-default dark:bg-muted border-subtle",
|
!isBackgroundTransparent && " bg-default dark:bg-muted border-booker border-booker-width",
|
||||||
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
"px-8 pt-5 pb-4 text-left align-bottom transition-all sm:w-full sm:py-8 sm:align-middle"
|
||||||
)}
|
)}
|
||||||
role="dialog"
|
role="dialog"
|
||||||
@@ -347,10 +350,10 @@ export default function Success(props: SuccessProps) {
|
|||||||
<img src={giphyImage} alt="Gif from Giphy" />
|
<img src={giphyImage} alt="Gif from Giphy" />
|
||||||
)}
|
)}
|
||||||
{!giphyImage && !needsConfirmation && !isCancelled && (
|
{!giphyImage && !needsConfirmation && !isCancelled && (
|
||||||
<FiCheck className="h-5 w-5 text-green-600" />
|
<Check className="h-5 w-5 text-green-600" />
|
||||||
)}
|
)}
|
||||||
{needsConfirmation && !isCancelled && <FiCalendar className="text-emphasis h-5 w-5" />}
|
{needsConfirmation && !isCancelled && <Calendar className="text-emphasis h-5 w-5" />}
|
||||||
{isCancelled && <FiX className="h-5 w-5 text-red-600" />}
|
{isCancelled && <X className="h-5 w-5 text-red-600" />}
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-6 mb-8 text-center last:mb-0">
|
<div className="mt-6 mb-8 text-center last:mb-0">
|
||||||
<h3
|
<h3
|
||||||
@@ -458,7 +461,7 @@ export default function Success(props: SuccessProps) {
|
|||||||
className="text-default flex items-center gap-2 underline"
|
className="text-default flex items-center gap-2 underline"
|
||||||
rel="noreferrer">
|
rel="noreferrer">
|
||||||
{providerName || "Link"}
|
{providerName || "Link"}
|
||||||
<FiExternalLink className="text-default inline h-4 w-4" />
|
<ExternalLink className="text-default inline h-4 w-4" />
|
||||||
</a>
|
</a>
|
||||||
) : (
|
) : (
|
||||||
locationToDisplay
|
locationToDisplay
|
||||||
@@ -706,6 +709,9 @@ export default function Success(props: SuccessProps) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Success.isBookingPage = true;
|
||||||
|
Success.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
type RecurringBookingsProps = {
|
type RecurringBookingsProps = {
|
||||||
eventType: SuccessProps["eventType"];
|
eventType: SuccessProps["eventType"];
|
||||||
recurringBookings: SuccessProps["recurringBookings"];
|
recurringBookings: SuccessProps["recurringBookings"];
|
||||||
@@ -914,7 +920,16 @@ const handleSeatsEventTypeOnBooking = (
|
|||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
|
const session = await getServerSession(context);
|
||||||
|
let tz: string | null = null;
|
||||||
|
|
||||||
|
if (session) {
|
||||||
|
const user = await ssr.viewer.me.fetch();
|
||||||
|
tz = user.timeZone;
|
||||||
|
}
|
||||||
|
|
||||||
const parsedQuery = querySchema.safeParse(context.query);
|
const parsedQuery = querySchema.safeParse(context.query);
|
||||||
|
|
||||||
if (!parsedQuery.success) return { notFound: true };
|
if (!parsedQuery.success) return { notFound: true };
|
||||||
const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
|
const { uid, email, eventTypeSlug, cancel, isSuccessBookingPage } = parsedQuery.data;
|
||||||
|
|
||||||
@@ -1046,6 +1061,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
|||||||
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
dynamicEventName: bookingInfo?.eventType?.eventName || "",
|
||||||
bookingInfo,
|
bookingInfo,
|
||||||
paymentStatus: payment,
|
paymentStatus: payment,
|
||||||
|
...(tz && { tz }),
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,10 +12,11 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
|||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
import { Alert, Button, EmptyScreen } from "@calcom/ui";
|
||||||
import { FiCalendar } from "@calcom/ui/components/icon";
|
import { Calendar } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
import { useInViewObserver } from "@lib/hooks/useInViewObserver";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import BookingListItem from "@components/booking/BookingListItem";
|
import BookingListItem from "@components/booking/BookingListItem";
|
||||||
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
import SkeletonLoader from "@components/booking/SkeletonLoader";
|
||||||
|
|
||||||
@@ -180,7 +181,7 @@ export default function Bookings() {
|
|||||||
{query.status === "success" && isEmpty && (
|
{query.status === "success" && isEmpty && (
|
||||||
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiCalendar}
|
Icon={Calendar}
|
||||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||||
description={t("no_status_bookings_yet_description", {
|
description={t("no_status_bookings_yet_description", {
|
||||||
status: t(status).toLowerCase(),
|
status: t(status).toLowerCase(),
|
||||||
@@ -194,6 +195,8 @@ export default function Bookings() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Bookings.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export const getStaticProps: GetStaticProps = async (ctx) => {
|
export const getStaticProps: GetStaticProps = async (ctx) => {
|
||||||
const params = querySchema.safeParse(ctx.params);
|
const params = querySchema.safeParse(ctx.params);
|
||||||
const ssg = await ssgInit(ctx);
|
const ssg = await ssgInit(ctx);
|
||||||
|
|||||||
@@ -14,6 +14,7 @@ import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
|||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
import type { EmbedProps } from "@lib/withEmbedSsr";
|
import type { EmbedProps } from "@lib/withEmbedSsr";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
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";
|
||||||
@@ -24,6 +25,9 @@ export default function Type(props: DynamicAvailabilityPageProps) {
|
|||||||
return <AvailabilityPage {...props} />;
|
return <AvailabilityPage {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Type.isBookingPage = true;
|
||||||
|
Type.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
const querySchema = z.object({
|
const querySchema = z.object({
|
||||||
link: z.string().optional().default(""),
|
link: z.string().optional().default(""),
|
||||||
slug: z.string().optional().default(""),
|
slug: z.string().optional().default(""),
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import { customInputSchema, eventTypeBookingFields, EventTypeMetaDataSchema } fr
|
|||||||
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrNull, asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import BookingPage from "@components/booking/pages/BookingPage";
|
import BookingPage from "@components/booking/pages/BookingPage";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
@@ -19,6 +20,9 @@ export default function Book(props: HashLinkPageProps) {
|
|||||||
return <BookingPage {...props} />;
|
return <BookingPage {...props} />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Book.isBookingPage = true;
|
||||||
|
Book.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
const ssr = await ssrInit(context);
|
const ssr = await ssrInit(context);
|
||||||
const link = asStringOrThrow(context.query.link as string);
|
const link = asStringOrThrow(context.query.link as string);
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import type { PeriodType } from "@prisma/client";
|
import type { PeriodType } from "@prisma/client";
|
||||||
import type { SchedulingType } from "@prisma/client";
|
import type { SchedulingType } from "@prisma/client";
|
||||||
import type { GetServerSidePropsContext } from "next";
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -11,26 +12,29 @@ import { z } from "zod";
|
|||||||
import { validateCustomEventName } from "@calcom/core/event";
|
import { validateCustomEventName } from "@calcom/core/event";
|
||||||
import type { EventLocationType } from "@calcom/core/location";
|
import type { EventLocationType } from "@calcom/core/location";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||||
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 { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||||
import { HttpError } from "@calcom/lib/http-error";
|
import { HttpError } from "@calcom/lib/http-error";
|
||||||
|
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
import type { IntervalLimit, RecurringEvent } from "@calcom/types/Calendar";
|
||||||
import { Form, showToast } from "@calcom/ui";
|
import { ConfirmationDialogContent, Dialog, Form, showToast } from "@calcom/ui";
|
||||||
|
|
||||||
import { asStringOrThrow } from "@lib/asStringOrNull";
|
import { asStringOrThrow } from "@lib/asStringOrNull";
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
|
// These can't really be moved into calcom/ui due to the fact they use infered getserverside props typings
|
||||||
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
|
import { EventAdvancedTab } from "@components/eventtype/EventAdvancedTab";
|
||||||
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
import { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
||||||
|
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
||||||
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||||
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
||||||
@@ -87,6 +91,7 @@ export type FormValues = {
|
|||||||
successRedirectUrl: string;
|
successRedirectUrl: string;
|
||||||
durationLimits?: IntervalLimit;
|
durationLimits?: IntervalLimit;
|
||||||
bookingLimits?: IntervalLimit;
|
bookingLimits?: IntervalLimit;
|
||||||
|
children: ChildrenEventType[];
|
||||||
hosts: { userId: number; isFixed: boolean }[];
|
hosts: { userId: number; isFixed: boolean }[];
|
||||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||||
};
|
};
|
||||||
@@ -111,10 +116,12 @@ const querySchema = z.object({
|
|||||||
});
|
});
|
||||||
|
|
||||||
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
||||||
|
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
|
||||||
|
|
||||||
const EventTypePage = (props: EventTypeSetupProps) => {
|
const EventTypePage = (props: EventTypeSetupProps) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
const telemetry = useTelemetry();
|
||||||
const {
|
const {
|
||||||
data: { tabName },
|
data: { tabName },
|
||||||
} = useTypedQuery(querySchema);
|
} = useTypedQuery(querySchema);
|
||||||
@@ -208,6 +215,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||||
metadata,
|
metadata,
|
||||||
hosts: eventType.hosts,
|
hosts: eventType.hosts,
|
||||||
|
children: eventType.children.map((ch) => ({
|
||||||
|
...ch,
|
||||||
|
created: true,
|
||||||
|
owner: {
|
||||||
|
...ch.owner,
|
||||||
|
eventTypeSlugs:
|
||||||
|
eventType.team?.members
|
||||||
|
.find((mem) => mem.user.id === ch.owner.id)
|
||||||
|
?.user.eventTypes.map((evTy) => evTy.slug)
|
||||||
|
.filter((slug) => slug !== eventType.slug) ?? [],
|
||||||
|
},
|
||||||
|
})),
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
const formMethods = useForm<FormValues>({
|
const formMethods = useForm<FormValues>({
|
||||||
@@ -266,8 +285,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
destinationCalendar={destinationCalendar}
|
destinationCalendar={destinationCalendar}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||||
team: <EventTeamTab teamMembers={teamMembers} team={team} />,
|
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||||
limits: <EventLimitsTab eventType={eventType} />,
|
limits: <EventLimitsTab eventType={eventType} />,
|
||||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||||
recurring: <EventRecurringTab eventType={eventType} />,
|
recurring: <EventRecurringTab eventType={eventType} />,
|
||||||
@@ -281,85 +300,143 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
|
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
|
const handleSubmit = async (values: FormValues) => {
|
||||||
|
const {
|
||||||
|
periodDates,
|
||||||
|
periodCountCalendarDays,
|
||||||
|
beforeBufferTime,
|
||||||
|
afterBufferTime,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees,
|
||||||
|
bookingLimits,
|
||||||
|
durationLimits,
|
||||||
|
recurringEvent,
|
||||||
|
locations,
|
||||||
|
metadata,
|
||||||
|
customInputs,
|
||||||
|
children,
|
||||||
|
// We don't need to send send these values to the backend
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
seatsPerTimeSlotEnabled,
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
|
minimumBookingNoticeInDurationType,
|
||||||
|
...input
|
||||||
|
} = values;
|
||||||
|
|
||||||
|
if (bookingLimits) {
|
||||||
|
const isValid = validateIntervalLimitOrder(bookingLimits);
|
||||||
|
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (durationLimits) {
|
||||||
|
const isValid = validateIntervalLimitOrder(durationLimits);
|
||||||
|
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.multipleDuration !== undefined) {
|
||||||
|
if (metadata?.multipleDuration.length < 1) {
|
||||||
|
throw new Error(t("event_setup_multiple_duration_error"));
|
||||||
|
} else {
|
||||||
|
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
||||||
|
throw new Error(t("event_setup_multiple_duration_default_error"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (metadata?.apps?.stripe?.paymentOption === "HOLD" && seatsPerTimeSlot) {
|
||||||
|
throw new Error(t("seats_and_no_show_fee_error"));
|
||||||
|
}
|
||||||
|
|
||||||
|
updateMutation.mutate({
|
||||||
|
...input,
|
||||||
|
locations,
|
||||||
|
recurringEvent,
|
||||||
|
periodStartDate: periodDates.startDate,
|
||||||
|
periodEndDate: periodDates.endDate,
|
||||||
|
periodCountCalendarDays: periodCountCalendarDays === "1",
|
||||||
|
id: eventType.id,
|
||||||
|
beforeEventBuffer: beforeBufferTime,
|
||||||
|
afterEventBuffer: afterBufferTime,
|
||||||
|
bookingLimits,
|
||||||
|
durationLimits,
|
||||||
|
seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees,
|
||||||
|
metadata,
|
||||||
|
customInputs,
|
||||||
|
children,
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const [slugExistsChildrenDialogOpen, setSlugExistsChildrenDialogOpen] = useState<ChildrenEventType[]>([]);
|
||||||
|
const slug = formMethods.watch("slug") ?? eventType.slug;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<EventTypeSingleLayout
|
<>
|
||||||
enabledAppsNumber={numberOfActiveApps}
|
<EventTypeSingleLayout
|
||||||
installedAppsNumber={numberOfInstalledApps}
|
enabledAppsNumber={numberOfActiveApps}
|
||||||
enabledWorkflowsNumber={eventType.workflows.length}
|
installedAppsNumber={numberOfInstalledApps}
|
||||||
eventType={eventType}
|
enabledWorkflowsNumber={eventType.workflows.length}
|
||||||
team={team}
|
eventType={eventType}
|
||||||
isUpdateMutationLoading={updateMutation.isLoading}
|
team={team}
|
||||||
formMethods={formMethods}
|
isUpdateMutationLoading={updateMutation.isLoading}
|
||||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
formMethods={formMethods}
|
||||||
currentUserMembership={currentUserMembership}>
|
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||||
<Form
|
currentUserMembership={currentUserMembership}>
|
||||||
form={formMethods}
|
<Form
|
||||||
id="event-type-form"
|
form={formMethods}
|
||||||
handleSubmit={async (values) => {
|
id="event-type-form"
|
||||||
const {
|
handleSubmit={async (values: FormValues) => {
|
||||||
periodDates,
|
if (!values.children.length) return handleSubmit(values);
|
||||||
periodCountCalendarDays,
|
const existingSlugEventTypes = values.children.filter((ch) =>
|
||||||
beforeBufferTime,
|
ch.owner.eventTypeSlugs.includes(slug)
|
||||||
afterBufferTime,
|
);
|
||||||
seatsPerTimeSlot,
|
if (!existingSlugEventTypes.length) return handleSubmit(values);
|
||||||
seatsShowAttendees,
|
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
|
||||||
bookingLimits,
|
}}>
|
||||||
durationLimits,
|
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||||
recurringEvent,
|
</Form>
|
||||||
locations,
|
</EventTypeSingleLayout>
|
||||||
metadata,
|
<Dialog
|
||||||
customInputs,
|
open={slugExistsChildrenDialogOpen.length > 0}
|
||||||
// We don't need to send send these values to the backend
|
onOpenChange={() => {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
setSlugExistsChildrenDialogOpen([]);
|
||||||
seatsPerTimeSlotEnabled,
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
||||||
minimumBookingNoticeInDurationType,
|
|
||||||
...input
|
|
||||||
} = values;
|
|
||||||
|
|
||||||
if (bookingLimits) {
|
|
||||||
const isValid = validateIntervalLimitOrder(bookingLimits);
|
|
||||||
if (!isValid) throw new Error(t("event_setup_booking_limits_error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (durationLimits) {
|
|
||||||
const isValid = validateIntervalLimitOrder(durationLimits);
|
|
||||||
if (!isValid) throw new Error(t("event_setup_duration_limits_error"));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (metadata?.multipleDuration !== undefined) {
|
|
||||||
if (metadata?.multipleDuration.length < 1) {
|
|
||||||
throw new Error(t("event_setup_multiple_duration_error"));
|
|
||||||
} else {
|
|
||||||
if (!input.length && !metadata?.multipleDuration?.includes(input.length)) {
|
|
||||||
throw new Error(t("event_setup_multiple_duration_default_error"));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
updateMutation.mutate({
|
|
||||||
...input,
|
|
||||||
locations,
|
|
||||||
recurringEvent,
|
|
||||||
periodStartDate: periodDates.startDate,
|
|
||||||
periodEndDate: periodDates.endDate,
|
|
||||||
periodCountCalendarDays: periodCountCalendarDays === "1",
|
|
||||||
id: eventType.id,
|
|
||||||
beforeEventBuffer: beforeBufferTime,
|
|
||||||
afterEventBuffer: afterBufferTime,
|
|
||||||
bookingLimits,
|
|
||||||
durationLimits,
|
|
||||||
seatsPerTimeSlot,
|
|
||||||
seatsShowAttendees,
|
|
||||||
metadata,
|
|
||||||
customInputs,
|
|
||||||
});
|
|
||||||
}}>
|
}}>
|
||||||
<div ref={animationParentRef} className="space-y-6">
|
<ConfirmationDialogContent
|
||||||
{tabMap[tabName]}
|
isLoading={formMethods.formState.isSubmitting}
|
||||||
</div>
|
variety="warning"
|
||||||
</Form>
|
title={t("managed_event_dialog_title", {
|
||||||
</EventTypeSingleLayout>
|
slug,
|
||||||
|
count: slugExistsChildrenDialogOpen.length,
|
||||||
|
})}
|
||||||
|
confirmBtnText={t("managed_event_dialog_confirm_button", {
|
||||||
|
count: slugExistsChildrenDialogOpen.length,
|
||||||
|
})}
|
||||||
|
cancelBtnText={t("go_back")}
|
||||||
|
onConfirm={(e: { preventDefault: () => void }) => {
|
||||||
|
e.preventDefault();
|
||||||
|
handleSubmit(formMethods.getValues());
|
||||||
|
telemetry.event(telemetryEventTypes.slugReplacementAction);
|
||||||
|
setSlugExistsChildrenDialogOpen([]);
|
||||||
|
}}>
|
||||||
|
<p className="mt-5">
|
||||||
|
<Trans
|
||||||
|
i18nKey="managed_event_dialog_information"
|
||||||
|
values={{
|
||||||
|
names: `${slugExistsChildrenDialogOpen
|
||||||
|
.map((ch) => ch.owner.name)
|
||||||
|
.slice(0, -1)
|
||||||
|
.join(", ")} ${
|
||||||
|
slugExistsChildrenDialogOpen.length > 1 ? t("and") : ""
|
||||||
|
} ${slugExistsChildrenDialogOpen.map((ch) => ch.owner.name).slice(-1)}`,
|
||||||
|
slug,
|
||||||
|
}}
|
||||||
|
count={slugExistsChildrenDialogOpen.length}
|
||||||
|
/>
|
||||||
|
</p>{" "}
|
||||||
|
<p className="mt-5">{t("managed_event_dialog_clarification")}</p>
|
||||||
|
</ConfirmationDialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -401,4 +478,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EventTypePageWrapper.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default EventTypePageWrapper;
|
export default EventTypePageWrapper;
|
||||||
@@ -1,4 +1,7 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
|
import type { User } from "@prisma/client";
|
||||||
|
import { SchedulingType } from "@prisma/client";
|
||||||
|
import { Trans } from "next-i18next";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/router";
|
import { useRouter } from "next/router";
|
||||||
import type { FC } from "react";
|
import type { FC } from "react";
|
||||||
@@ -39,25 +42,26 @@ import {
|
|||||||
HeadSeo,
|
HeadSeo,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
FiArrowDown,
|
ArrowDown,
|
||||||
FiArrowUp,
|
ArrowUp,
|
||||||
FiClipboard,
|
Clipboard,
|
||||||
FiCode,
|
Code,
|
||||||
FiCopy,
|
Copy,
|
||||||
FiEdit,
|
Edit,
|
||||||
FiEdit2,
|
Edit2,
|
||||||
FiExternalLink,
|
ExternalLink,
|
||||||
FiLink,
|
Link as LinkIcon,
|
||||||
FiMoreHorizontal,
|
MoreHorizontal,
|
||||||
FiTrash,
|
Trash,
|
||||||
FiUpload,
|
Upload,
|
||||||
FiUsers,
|
Users,
|
||||||
} from "@calcom/ui/components/icon";
|
} from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { withQuery } from "@lib/QueryCell";
|
||||||
import { HttpError } from "@lib/core/http/error";
|
import { HttpError } from "@lib/core/http/error";
|
||||||
|
|
||||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
@@ -116,7 +120,40 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
|||||||
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
|
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
return (
|
const content = () => (
|
||||||
|
<div>
|
||||||
|
<span
|
||||||
|
className="text-default font-semibold ltr:mr-1 rtl:ml-1"
|
||||||
|
data-testid={"event-type-title-" + type.id}>
|
||||||
|
{type.title}
|
||||||
|
</span>
|
||||||
|
{group.profile.slug ? (
|
||||||
|
<small
|
||||||
|
className="text-subtle hidden font-normal leading-4 sm:inline"
|
||||||
|
data-testid={"event-type-slug-" + type.id}>
|
||||||
|
{`/${
|
||||||
|
type.schedulingType !== SchedulingType.MANAGED ? group.profile.slug : t("username_placeholder")
|
||||||
|
}/${type.slug}`}
|
||||||
|
</small>
|
||||||
|
) : null}
|
||||||
|
{readOnly && (
|
||||||
|
<Badge variant="gray" className="ml-2">
|
||||||
|
{t("readonly")}
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
return readOnly ? (
|
||||||
|
<div className="flex-1 overflow-hidden pr-4 text-sm">
|
||||||
|
{content()}
|
||||||
|
<EventTypeDescription
|
||||||
|
// @ts-expect-error FIXME: We have a type mismatch here @hariombalhara @sean-brydon
|
||||||
|
eventType={type}
|
||||||
|
shortenDescription
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
<Link
|
<Link
|
||||||
href={`/event-types/${type.id}?tabName=setup`}
|
href={`/event-types/${type.id}?tabName=setup`}
|
||||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||||
@@ -157,6 +194,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||||
|
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||||
|
null
|
||||||
|
);
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||||
onError: async (err) => {
|
onError: async (err) => {
|
||||||
@@ -317,6 +357,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
{types.map((type, index) => {
|
{types.map((type, index) => {
|
||||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||||
const calLink = `${CAL_URL}/${embedLink}`;
|
const calLink = `${CAL_URL}/${embedLink}`;
|
||||||
|
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||||
|
const isChildrenManagedEventType =
|
||||||
|
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||||
return (
|
return (
|
||||||
<li key={type.id}>
|
<li key={type.id}>
|
||||||
<div className="hover:bg-muted flex w-full items-center justify-between">
|
<div className="hover:bg-muted flex w-full items-center justify-between">
|
||||||
@@ -325,7 +368,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<button
|
<button
|
||||||
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
className="bg-default text-muted hover:text-emphasis border-default hover:border-emphasis invisible absolute left-[5px] -mt-4 mb-4 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||||
onClick={() => moveEventType(index, -1)}>
|
onClick={() => moveEventType(index, -1)}>
|
||||||
<FiArrowUp className="h-5 w-5" />
|
<ArrowUp className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
@@ -333,116 +376,148 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<button
|
<button
|
||||||
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
className="bg-default text-muted border-default hover:text-emphasis hover:border-emphasis invisible absolute left-[5px] mt-8 -ml-4 hidden h-6 w-6 scale-0 items-center justify-center rounded-md border p-1 transition-all group-hover:visible group-hover:scale-100 sm:ml-0 sm:flex lg:left-[36px]"
|
||||||
onClick={() => moveEventType(index, 1)}>
|
onClick={() => moveEventType(index, 1)}>
|
||||||
<FiArrowDown className="h-5 w-5" />
|
<ArrowDown className="h-5 w-5" />
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{type.team && (
|
{type.team && !isManagedEventType && (
|
||||||
<AvatarGroup
|
<AvatarGroup
|
||||||
className="relative top-1 right-3"
|
className="relative top-1 right-3"
|
||||||
size="sm"
|
size="sm"
|
||||||
truncateAfter={4}
|
truncateAfter={4}
|
||||||
items={type.users.map((organizer) => ({
|
items={type.users.map((organizer: { name: any; username: any }) => ({
|
||||||
alt: organizer.name || "",
|
alt: organizer.name || "",
|
||||||
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
||||||
title: organizer.name || "",
|
title: organizer.name || "",
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
|
{isManagedEventType && (
|
||||||
|
<AvatarGroup
|
||||||
|
className="relative top-1 right-3"
|
||||||
|
size="sm"
|
||||||
|
truncateAfter={4}
|
||||||
|
items={type.children
|
||||||
|
.flatMap((ch) => ch.users)
|
||||||
|
.map((user: User) => ({
|
||||||
|
alt: user.name || "",
|
||||||
|
image: `${WEBAPP_URL}/${user.username}/avatar.png`,
|
||||||
|
title: user.name || "",
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
<div className="flex items-center justify-between space-x-2 rtl:space-x-reverse">
|
||||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
{!isManagedEventType && (
|
||||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
<>
|
||||||
<div className="self-center rounded-md p-2">
|
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||||
<Switch
|
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||||
name="Hidden"
|
<div className="self-center rounded-md p-2">
|
||||||
checked={!type.hidden}
|
<Switch
|
||||||
onCheckedChange={() => {
|
name="Hidden"
|
||||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
checked={!type.hidden}
|
||||||
}}
|
onCheckedChange={() => {
|
||||||
/>
|
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||||
</div>
|
}}
|
||||||
</Tooltip>
|
/>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
|
||||||
<ButtonGroup combined>
|
<ButtonGroup combined>
|
||||||
<Tooltip content={t("preview")}>
|
{!isManagedEventType && (
|
||||||
<Button
|
<>
|
||||||
data-testid="preview-link-button"
|
<Tooltip content={t("preview")}>
|
||||||
color="secondary"
|
<Button
|
||||||
target="_blank"
|
data-testid="preview-link-button"
|
||||||
variant="icon"
|
color="secondary"
|
||||||
href={calLink}
|
target="_blank"
|
||||||
StartIcon={FiExternalLink}
|
variant="icon"
|
||||||
/>
|
href={calLink}
|
||||||
</Tooltip>
|
StartIcon={ExternalLink}
|
||||||
|
/>
|
||||||
|
</Tooltip>
|
||||||
|
|
||||||
<Tooltip content={t("copy_link")}>
|
<Tooltip content={t("copy_link")}>
|
||||||
<Button
|
<Button
|
||||||
color="secondary"
|
color="secondary"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
StartIcon={FiLink}
|
StartIcon={LinkIcon}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
navigator.clipboard.writeText(calLink);
|
navigator.clipboard.writeText(calLink);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</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}>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
StartIcon={FiMoreHorizontal}
|
StartIcon={MoreHorizontal}
|
||||||
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
|
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
|
||||||
/>
|
/>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
<DropdownMenuItem>
|
{!readOnly && (
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
data-testid={"event-type-edit-" + type.id}
|
|
||||||
StartIcon={FiEdit2}
|
|
||||||
onClick={() => router.push("/event-types/" + type.id)}>
|
|
||||||
{t("edit")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="outline-none">
|
|
||||||
<DropdownItem
|
|
||||||
type="button"
|
|
||||||
data-testid={"event-type-duplicate-" + type.id}
|
|
||||||
StartIcon={FiCopy}
|
|
||||||
onClick={() => openDuplicateModal(type, group)}>
|
|
||||||
{t("duplicate")}
|
|
||||||
</DropdownItem>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem className="outline-none">
|
|
||||||
<EmbedButton
|
|
||||||
as={DropdownItem}
|
|
||||||
type="button"
|
|
||||||
StartIcon={FiCode}
|
|
||||||
className="w-full rounded-none"
|
|
||||||
embedUrl={encodeURIComponent(embedLink)}>
|
|
||||||
{t("embed")}
|
|
||||||
</EmbedButton>
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuSeparator />
|
|
||||||
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
|
||||||
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) && (
|
|
||||||
<DropdownMenuItem>
|
<DropdownMenuItem>
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
color="destructive"
|
type="button"
|
||||||
onClick={() => {
|
data-testid={"event-type-edit-" + type.id}
|
||||||
setDeleteDialogOpen(true);
|
StartIcon={Edit2}
|
||||||
setDeleteDialogTypeId(type.id);
|
onClick={() => router.push("/event-types/" + type.id)}>
|
||||||
}}
|
{t("edit")}
|
||||||
StartIcon={FiTrash}
|
|
||||||
className="w-full rounded-none">
|
|
||||||
{t("delete")}
|
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
)}
|
)}
|
||||||
|
{!isManagedEventType && !isChildrenManagedEventType && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem className="outline-none">
|
||||||
|
<DropdownItem
|
||||||
|
type="button"
|
||||||
|
data-testid={"event-type-duplicate-" + type.id}
|
||||||
|
StartIcon={Copy}
|
||||||
|
onClick={() => openDuplicateModal(type, group)}>
|
||||||
|
{t("duplicate")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem className="outline-none">
|
||||||
|
<EmbedButton
|
||||||
|
as={DropdownItem}
|
||||||
|
type="button"
|
||||||
|
StartIcon={Code}
|
||||||
|
className="w-full rounded-none"
|
||||||
|
embedUrl={encodeURIComponent(embedLink)}>
|
||||||
|
{t("embed")}
|
||||||
|
</EmbedButton>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
{/* readonly is only set when we are on a team - if we are on a user event type null will be the value. */}
|
||||||
|
{(group.metadata?.readOnly === false || group.metadata.readOnly === null) &&
|
||||||
|
isManagedEventType && (
|
||||||
|
<>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem>
|
||||||
|
<DropdownItem
|
||||||
|
color="destructive"
|
||||||
|
onClick={() => {
|
||||||
|
setDeleteDialogOpen(true);
|
||||||
|
setDeleteDialogTypeId(type.id);
|
||||||
|
setDeleteDialogSchedulingType(type.schedulingType);
|
||||||
|
}}
|
||||||
|
StartIcon={Trash}
|
||||||
|
className="w-full rounded-none">
|
||||||
|
{t("delete")}
|
||||||
|
</DropdownItem>
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
</ButtonGroup>
|
</ButtonGroup>
|
||||||
@@ -453,7 +528,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||||
<Dropdown>
|
<Dropdown>
|
||||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
||||||
<Button type="button" variant="icon" color="secondary" StartIcon={FiMoreHorizontal} />
|
<Button type="button" variant="icon" color="secondary" StartIcon={MoreHorizontal} />
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
<DropdownMenuPortal>
|
<DropdownMenuPortal>
|
||||||
<DropdownMenuContent>
|
<DropdownMenuContent>
|
||||||
@@ -461,7 +536,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<DropdownItem
|
<DropdownItem
|
||||||
href={calLink}
|
href={calLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
StartIcon={FiExternalLink}
|
StartIcon={ExternalLink}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("preview")}
|
{t("preview")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -473,7 +548,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
navigator.clipboard.writeText(calLink);
|
navigator.clipboard.writeText(calLink);
|
||||||
showToast(t("link_copied"), "success");
|
showToast(t("link_copied"), "success");
|
||||||
}}
|
}}
|
||||||
StartIcon={FiClipboard}
|
StartIcon={Clipboard}
|
||||||
className="w-full rounded-none text-left">
|
className="w-full rounded-none text-left">
|
||||||
{t("copy_link")}
|
{t("copy_link")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -492,7 +567,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
.then(() => showToast(t("link_shared"), "success"))
|
.then(() => showToast(t("link_shared"), "success"))
|
||||||
.catch(() => showToast(t("failed"), "error"));
|
.catch(() => showToast(t("failed"), "error"));
|
||||||
}}
|
}}
|
||||||
StartIcon={FiUpload}
|
StartIcon={Upload}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("share")}
|
{t("share")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -501,7 +576,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => router.push("/event-types/" + type.id)}
|
onClick={() => router.push("/event-types/" + type.id)}
|
||||||
StartIcon={FiEdit}
|
StartIcon={Edit}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("edit")}
|
{t("edit")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -509,7 +584,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<DropdownMenuItem className="outline-none">
|
<DropdownMenuItem className="outline-none">
|
||||||
<DropdownItem
|
<DropdownItem
|
||||||
onClick={() => openDuplicateModal(type, group)}
|
onClick={() => openDuplicateModal(type, group)}
|
||||||
StartIcon={FiCopy}
|
StartIcon={Copy}
|
||||||
data-testid={"event-type-duplicate-" + type.id}>
|
data-testid={"event-type-duplicate-" + type.id}>
|
||||||
{t("duplicate")}
|
{t("duplicate")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -521,8 +596,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
onClick={() => {
|
onClick={() => {
|
||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
setDeleteDialogTypeId(type.id);
|
setDeleteDialogTypeId(type.id);
|
||||||
|
setDeleteDialogSchedulingType(type.schedulingType);
|
||||||
}}
|
}}
|
||||||
StartIcon={FiTrash}
|
StartIcon={Trash}
|
||||||
className="w-full rounded-none">
|
className="w-full rounded-none">
|
||||||
{t("delete")}
|
{t("delete")}
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
@@ -539,14 +615,37 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_event_type")}
|
title={t(
|
||||||
confirmBtnText={t("confirm_delete_event_type")}
|
`delete_${deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"}_event_type`
|
||||||
loadingText={t("confirm_delete_event_type")}
|
)}
|
||||||
|
confirmBtnText={t(
|
||||||
|
`confirm_delete_${
|
||||||
|
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||||
|
}_event_type`
|
||||||
|
)}
|
||||||
|
loadingText={t(
|
||||||
|
`confirm_delete_${
|
||||||
|
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||||
|
}_event_type`
|
||||||
|
)}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteEventTypeHandler(deleteDialogTypeId);
|
deleteEventTypeHandler(deleteDialogTypeId);
|
||||||
}}>
|
}}>
|
||||||
{t("delete_event_type_description")}
|
<p className="mt-5">
|
||||||
|
{t(
|
||||||
|
`delete_${
|
||||||
|
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||||
|
}_event_type_description`
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="mt-5">
|
||||||
|
<Trans
|
||||||
|
i18nKey={`delete_${
|
||||||
|
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||||
|
}_event_type_warning`}
|
||||||
|
/>
|
||||||
|
</p>
|
||||||
</ConfirmationDialogContent>
|
</ConfirmationDialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
@@ -589,7 +688,7 @@ const EventTypeListHeading = ({
|
|||||||
<span className="text-subtle ms-2 me-2 relative -top-px text-xs">
|
<span className="text-subtle ms-2 me-2 relative -top-px text-xs">
|
||||||
<Link href={`/settings/teams/${teamId}/members`}>
|
<Link href={`/settings/teams/${teamId}/members`}>
|
||||||
<Badge variant="gray">
|
<Badge variant="gray">
|
||||||
<FiUsers className="mr-1 -mt-px inline h-3 w-3" />
|
<Users className="mr-1 -mt-px inline h-3 w-3" />
|
||||||
{membershipCount}
|
{membershipCount}
|
||||||
</Badge>
|
</Badge>
|
||||||
</Link>
|
</Link>
|
||||||
@@ -617,7 +716,7 @@ const CreateFirstEventTypeView = () => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={FiLink}
|
Icon={LinkIcon}
|
||||||
headline={t("new_event_type_heading")}
|
headline={t("new_event_type_heading")}
|
||||||
description={t("new_event_type_description")}
|
description={t("new_event_type_description")}
|
||||||
/>
|
/>
|
||||||
@@ -638,6 +737,7 @@ const CTA = () => {
|
|||||||
teamId: profile.teamId,
|
teamId: profile.teamId,
|
||||||
label: profile.name || profile.slug,
|
label: profile.name || profile.slug,
|
||||||
image: profile.image,
|
image: profile.image,
|
||||||
|
membershipRole: profile.membershipRole,
|
||||||
slug: profile.slug,
|
slug: profile.slug,
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
@@ -646,7 +746,7 @@ const CTA = () => {
|
|||||||
<CreateButton
|
<CreateButton
|
||||||
subtitle={t("create_event_on").toUpperCase()}
|
subtitle={t("create_event_on").toUpperCase()}
|
||||||
options={profileOptions}
|
options={profileOptions}
|
||||||
createDialog={CreateEventTypeDialog}
|
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
@@ -718,4 +818,6 @@ const EventTypesPage = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
EventTypesPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default EventTypesPage;
|
export default EventTypesPage;
|
||||||
@@ -13,6 +13,7 @@ import { Button, StepCard, Steps } from "@calcom/ui";
|
|||||||
|
|
||||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
import { ConnectedCalendars } from "@components/getting-started/steps-views/ConnectCalendars";
|
||||||
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
import { SetupAvailability } from "@components/getting-started/steps-views/SetupAvailability";
|
||||||
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
import UserProfile from "@components/getting-started/steps-views/UserProfile";
|
||||||
@@ -203,5 +204,6 @@ export const getServerSideProps = async (context: GetServerSidePropsContext) =>
|
|||||||
};
|
};
|
||||||
|
|
||||||
OnboardingPage.isThemeSupported = false;
|
OnboardingPage.isThemeSupported = false;
|
||||||
|
OnboardingPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default OnboardingPage;
|
export default OnboardingPage;
|
||||||
@@ -15,7 +15,9 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { trpc } from "@calcom/trpc";
|
import { trpc } from "@calcom/trpc";
|
||||||
import { Button, ButtonGroup } from "@calcom/ui";
|
import { Button, ButtonGroup } from "@calcom/ui";
|
||||||
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
|
import { RefreshCcw, UserPlus, Users } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
const Heading = () => {
|
const Heading = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -35,17 +37,17 @@ export default function InsightsPage() {
|
|||||||
const { data: user } = trpc.viewer.me.useQuery();
|
const { data: user } = trpc.viewer.me.useQuery();
|
||||||
const features = [
|
const features = [
|
||||||
{
|
{
|
||||||
icon: <FiUsers className="h-5 w-5" />,
|
icon: <Users className="h-5 w-5" />,
|
||||||
title: t("view_bookings_across"),
|
title: t("view_bookings_across"),
|
||||||
description: t("view_bookings_across_description"),
|
description: t("view_bookings_across_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FiRefreshCcw className="h-5 w-5" />,
|
icon: <RefreshCcw className="h-5 w-5" />,
|
||||||
title: t("identify_booking_trends"),
|
title: t("identify_booking_trends"),
|
||||||
description: t("identify_booking_trends_description"),
|
description: t("identify_booking_trends_description"),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
icon: <FiUserPlus className="h-5 w-5" />,
|
icon: <UserPlus className="h-5 w-5" />,
|
||||||
title: t("spot_popular_event_types"),
|
title: t("spot_popular_event_types"),
|
||||||
description: t("spot_popular_event_types_description"),
|
description: t("spot_popular_event_types_description"),
|
||||||
},
|
},
|
||||||
@@ -111,6 +113,8 @@ export default function InsightsPage() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
InsightsPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
// If feature flag is disabled, return not found on getServerSideProps
|
// If feature flag is disabled, return not found on getServerSideProps
|
||||||
export const getServerSideProps = async () => {
|
export const getServerSideProps = async () => {
|
||||||
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
const prisma = await import("@calcom/prisma").then((mod) => mod.default);
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function MaintenancePage() {
|
export default function MaintenancePage() {
|
||||||
const { t, isLocaleReady } = useLocale();
|
const { t, isLocaleReady } = useLocale();
|
||||||
if (!isLocaleReady) return null;
|
if (!isLocaleReady) return null;
|
||||||
@@ -25,3 +27,5 @@ export default function MaintenancePage() {
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
MaintenancePage.PageWrapper = PageWrapper;
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
import Shell, { MobileNavigationMoreItems } from "@calcom/features/shell/Shell";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function MorePage() {
|
export default function MorePage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
return (
|
return (
|
||||||
@@ -12,3 +14,4 @@ export default function MorePage() {
|
|||||||
</Shell>
|
</Shell>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
MorePage.PageWrapper = PageWrapper;
|
||||||
@@ -2,8 +2,10 @@ import PaymentPage from "@calcom/features/ee/payments/components/PaymentPage";
|
|||||||
import { getServerSideProps } from "@calcom/features/ee/payments/pages/payment";
|
import { getServerSideProps } from "@calcom/features/ee/payments/pages/payment";
|
||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
export default function Payment(props: inferSSRProps<typeof getServerSideProps>) {
|
export default function Payment(props: inferSSRProps<typeof getServerSideProps>) {
|
||||||
return <PaymentPage {...props} />;
|
return <PaymentPage {...props} />;
|
||||||
}
|
}
|
||||||
|
Payment.PageWrapper = PageWrapper;
|
||||||
export { getServerSideProps };
|
export { getServerSideProps };
|
||||||
@@ -2,6 +2,7 @@ import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { Meta } from "@calcom/ui";
|
import { Meta } from "@calcom/ui";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
function AdminAppsView() {
|
function AdminAppsView() {
|
||||||
@@ -15,5 +16,6 @@ function AdminAppsView() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
AdminAppsView.getLayout = getLayout;
|
AdminAppsView.getLayout = getLayout;
|
||||||
|
AdminAppsView.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default AdminAppsView;
|
export default AdminAppsView;
|
||||||
@@ -1,9 +1,11 @@
|
|||||||
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
import { FlagListingView } from "@calcom/features/flags/pages/flag-listing-view";
|
||||||
|
|
||||||
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
import { getLayout } from "@components/auth/layouts/AdminLayout";
|
||||||
|
|
||||||
const FlagsPage = () => <FlagListingView />;
|
const FlagsPage = () => <FlagListingView />;
|
||||||
|
|
||||||
FlagsPage.getLayout = getLayout;
|
FlagsPage.getLayout = getLayout;
|
||||||
|
FlagsPage.PageWrapper = PageWrapper;
|
||||||
|
|
||||||
export default FlagsPage;
|
export default FlagsPage;
|
||||||
Loaded 100 of 498 files, more files were not shown because too many files have changed in this diff.
Show more
Reference in new issue
Block a user