Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -177,3 +177,5 @@ CSP_POLICY=
|
||||
|
||||
# Vercel 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,
|
||||
# @global-owner1 and @global-owner2 will be requested for
|
||||
# review when someone opens a pull request.
|
||||
* @calcom/reviewers
|
||||
# ⬇️ Removed so we can manually trigger checks
|
||||
# * @calcom/reviewers
|
||||
|
||||
.github/ @calcom/core
|
||||
.husky/ @calcom/core
|
||||
|
||||
@@ -15,6 +15,9 @@ jobs:
|
||||
steps:
|
||||
- uses: actions/stale@v7
|
||||
with:
|
||||
days-before-stale: 60
|
||||
include-only-assigned: true
|
||||
days-before-close: -1
|
||||
days-before-issue-stale: 60
|
||||
days-before-issue-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.
|
||||
|
||||
@@ -2,6 +2,7 @@ name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [ready_for_review, review_requested]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
|
||||
@@ -103,59 +103,6 @@ paths:
|
||||
summary: Reset a user's password
|
||||
tags:
|
||||
- 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:
|
||||
get:
|
||||
description: Gets the user's selected calendars.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { useState } from "react";
|
||||
|
||||
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() {
|
||||
const { t } = useLocale();
|
||||
@@ -40,7 +40,7 @@ export default function AddToHomescreen() {
|
||||
type="button"
|
||||
className="-mr-1 flex rounded-md p-2 hover:bg-gray-800 focus:outline-none focus:ring-2 focus:ring-white">
|
||||
<span className="sr-only">{t("dismiss")}</span>
|
||||
<FiX className="text-inverted h-6 w-6" aria-hidden="true" />
|
||||
<X className="text-inverted h-6 w-6" aria-hidden="true" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { z } from "zod";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { Badge, ListItemText } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
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>
|
||||
{invalidCredential && (
|
||||
<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">
|
||||
{t("invalid_credential")}
|
||||
</ListItemText>
|
||||
|
||||
@@ -22,7 +22,7 @@ import {
|
||||
TextField,
|
||||
ColorPicker,
|
||||
} 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";
|
||||
|
||||
@@ -493,7 +493,7 @@ const tabs = [
|
||||
{
|
||||
name: "HTML",
|
||||
href: "embedTabName=embed-code",
|
||||
icon: FiCode,
|
||||
icon: Code,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -546,7 +546,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "HTML", embedType, calLink, previ
|
||||
{
|
||||
name: "React",
|
||||
href: "embedTabName=embed-react",
|
||||
icon: FiCode,
|
||||
icon: Code,
|
||||
type: "code",
|
||||
Component: forwardRef<
|
||||
HTMLTextAreaElement | HTMLIFrameElement | null,
|
||||
@@ -586,7 +586,7 @@ ${getEmbedTypeSpecificString({ embedFramework: "react", embedType, calLink, prev
|
||||
{
|
||||
name: "Preview",
|
||||
href: "embedTabName=embed-preview",
|
||||
icon: FiTrello,
|
||||
icon: Trello,
|
||||
type: "iframe",
|
||||
Component: forwardRef<
|
||||
HTMLIFrameElement | HTMLTextAreaElement | null,
|
||||
@@ -622,7 +622,7 @@ Cal("init", {origin:"${WEBAPP_URL}"});
|
||||
const ThemeSelectControl = ({ children, ...props }: ControlProps<{ value: Theme; label: string }, false>) => {
|
||||
return (
|
||||
<components.Control {...props}>
|
||||
<FiSun className="text-subtle ml-2 h-4 w-4" />
|
||||
<Sun className="text-subtle ml-2 h-4 w-4" />
|
||||
{children}
|
||||
</components.Control>
|
||||
);
|
||||
@@ -830,7 +830,7 @@ const EmbedTypeCodeAndPreviewDialogContent = ({
|
||||
onClick={() => {
|
||||
removeQueryParams(router, ["embedType", "embedTabName"]);
|
||||
}}>
|
||||
<FiArrowLeft className="mr-4 w-4" />
|
||||
<ArrowLeft className="mr-4 w-4" />
|
||||
</button>
|
||||
{embed.title}
|
||||
</h3>
|
||||
|
||||
@@ -3,7 +3,7 @@ import React from "react";
|
||||
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
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";
|
||||
|
||||
@@ -11,32 +11,32 @@ const tabs = [
|
||||
{
|
||||
name: "profile",
|
||||
href: "/settings/my-account/profile",
|
||||
icon: FiUser,
|
||||
icon: User,
|
||||
},
|
||||
{
|
||||
name: "teams",
|
||||
href: "/settings/teams",
|
||||
icon: FiUsers,
|
||||
icon: Users,
|
||||
},
|
||||
{
|
||||
name: "security",
|
||||
href: "/settings/security",
|
||||
icon: FiKey,
|
||||
icon: Key,
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
href: "/settings/developer",
|
||||
icon: FiTerminal,
|
||||
icon: Terminal,
|
||||
},
|
||||
{
|
||||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: FiCreditCard,
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "admin",
|
||||
href: "/settings/admin",
|
||||
icon: FiLock,
|
||||
icon: Lock,
|
||||
adminRequired: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -9,7 +9,7 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownItem,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -40,7 +40,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
return (
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={FiPlus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -48,7 +48,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
||||
{options.map((data) => (
|
||||
<DropdownMenuItem key={data.slug} className="focus:outline-none">
|
||||
{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")}
|
||||
</DropdownItem>
|
||||
) : (
|
||||
|
||||
@@ -14,16 +14,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { App as AppType } from "@calcom/types/App";
|
||||
import { Button, showToast, SkeletonButton, SkeletonText, HeadSeo, Badge } from "@calcom/ui";
|
||||
import {
|
||||
FiBookOpen,
|
||||
FiCheck,
|
||||
FiExternalLink,
|
||||
FiFile,
|
||||
FiFlag,
|
||||
FiMail,
|
||||
FiPlus,
|
||||
FiShield,
|
||||
} from "@calcom/ui/components/icon";
|
||||
import { BookOpen, Check, ExternalLink, File, Flag, Mail, Plus, Shield } from "@calcom/ui/components/icon";
|
||||
|
||||
/* These app slugs all require Google Cal to be installed */
|
||||
|
||||
@@ -153,7 +144,7 @@ const Component = ({
|
||||
isGlobal ||
|
||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||
<div className="flex space-x-3">
|
||||
<Button StartIcon={FiCheck} color="secondary" disabled>
|
||||
<Button StartIcon={Check} color="secondary" disabled>
|
||||
{existingCredentials.length > 0
|
||||
? t("active_install", { count: existingCredentials.length })
|
||||
: t("default")}
|
||||
@@ -175,7 +166,7 @@ const Component = ({
|
||||
}
|
||||
return (
|
||||
<Button
|
||||
StartIcon={FiPlus}
|
||||
StartIcon={Plus}
|
||||
{...props}
|
||||
// @TODO: Overriding color and size prevent us from
|
||||
// having to duplicate InstallAppButton for now.
|
||||
@@ -275,7 +266,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis text-sm font-normal no-underline hover:underline"
|
||||
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")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -287,7 +278,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
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://", "")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -299,7 +290,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
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}
|
||||
</a>
|
||||
@@ -312,7 +303,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
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")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -324,7 +315,7 @@ const Component = ({
|
||||
rel="noreferrer"
|
||||
className="text-emphasis font-normal no-underline hover:underline"
|
||||
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")}
|
||||
</a>
|
||||
</li>
|
||||
@@ -335,7 +326,7 @@ const Component = ({
|
||||
{t("every_app_published", { appName: APP_NAME, companyName: COMPANY_NAME })}
|
||||
</span>
|
||||
<a className="mt-2 block text-xs text-red-500" href={`mailto:${SUPPORT_MAIL_ADDRESS}`}>
|
||||
<FiFlag className="inline h-3 w-3" /> {t("report_app")}
|
||||
<Flag className="inline h-3 w-3" /> {t("report_app")}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -17,7 +17,7 @@ import {
|
||||
Switch,
|
||||
ShellSubHeading,
|
||||
} 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";
|
||||
|
||||
@@ -100,7 +100,7 @@ function CalendarSwitch(props: {
|
||||
/>
|
||||
{!!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">
|
||||
<FiArrowLeft className="h-4 w-4" />
|
||||
<ArrowLeft className="h-4 w-4" />
|
||||
{t("adding_events_to")}
|
||||
</span>
|
||||
)}
|
||||
@@ -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="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">
|
||||
<FiCalendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||
<FiPlus
|
||||
<Calendar className="text-default h-8 w-8" strokeWidth="1" />
|
||||
<Plus
|
||||
className="text-emphasis absolute left-4 top-1/2 ml-0.5 mt-[1px] h-2 w-2"
|
||||
strokeWidth="4"
|
||||
/>
|
||||
@@ -322,7 +322,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={FiCalendar}
|
||||
Icon={Calendar}
|
||||
headline={t("no_category_apps", {
|
||||
category: t("calendar").toLowerCase(),
|
||||
})}
|
||||
|
||||
@@ -6,7 +6,7 @@ import React from "react";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiAlertCircle } from "@calcom/ui/components/icon";
|
||||
import { AlertCircle } from "@calcom/ui/components/icon";
|
||||
|
||||
type AppsLayoutProps = {
|
||||
children: React.ReactNode;
|
||||
@@ -27,7 +27,7 @@ export default function AppsLayout({ children, actions, emptyStore, ...rest }: A
|
||||
<main className="w-full">
|
||||
{emptyStore ? (
|
||||
<EmptyScreen
|
||||
Icon={FiAlertCircle}
|
||||
Icon={AlertCircle}
|
||||
headline={t("no_apps")}
|
||||
description={session.data?.user.role === "ADMIN" ? "You can enable apps in the settings" : ""}
|
||||
buttonText={session.data?.user.role === "ADMIN" ? t("apps_settings") : ""}
|
||||
|
||||
@@ -6,38 +6,38 @@ import type { InstalledAppVariants } from "@calcom/app-store/utils";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
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)[] = [
|
||||
{
|
||||
name: "calendar",
|
||||
href: "/apps/installed/calendar",
|
||||
icon: FiCalendar,
|
||||
icon: Calendar,
|
||||
},
|
||||
{
|
||||
name: "conferencing",
|
||||
href: "/apps/installed/conferencing",
|
||||
icon: FiVideo,
|
||||
icon: Video,
|
||||
},
|
||||
{
|
||||
name: "payment",
|
||||
href: "/apps/installed/payment",
|
||||
icon: FiCreditCard,
|
||||
icon: CreditCard,
|
||||
},
|
||||
{
|
||||
name: "automation",
|
||||
href: "/apps/installed/automation",
|
||||
icon: FiShare2,
|
||||
icon: Share2,
|
||||
},
|
||||
{
|
||||
name: "analytics",
|
||||
href: "/apps/installed/analytics",
|
||||
icon: FiBarChart,
|
||||
icon: BarChart,
|
||||
},
|
||||
{
|
||||
name: "other",
|
||||
href: "/apps/installed/other",
|
||||
icon: FiGrid,
|
||||
icon: Grid,
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import React from "react";
|
||||
|
||||
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";
|
||||
|
||||
@@ -31,7 +31,7 @@ function SkeletonItem() {
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon={FiMoreHorizontal}
|
||||
StartIcon={MoreHorizontal}
|
||||
disabled
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getEventLocationType, locationKeyToString } from "@calcom/app-store/loc
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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";
|
||||
|
||||
@@ -41,7 +41,7 @@ export function AvailableEventLocations({ locations }: { locations: Props["event
|
||||
return (
|
||||
<div key={`${location.type}-${index}`} className="flex flex-row items-center text-sm font-medium">
|
||||
{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
|
||||
src={eventLocationType.iconUrl}
|
||||
|
||||
@@ -10,6 +10,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useMediaQuery from "@calcom/lib/hooks/useMediaQuery";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { nameOfDay } from "@calcom/lib/weekday";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { Slot } from "@calcom/trpc/server/routers/viewer/slots";
|
||||
import { SkeletonContainer, SkeletonText, ToggleGroup } from "@calcom/ui";
|
||||
|
||||
@@ -28,6 +29,7 @@ type AvailableTimesProps = {
|
||||
slots?: Slot[];
|
||||
isLoading: boolean;
|
||||
ethSignature?: string;
|
||||
duration: number;
|
||||
};
|
||||
|
||||
const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
@@ -42,7 +44,9 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
seatsPerTimeSlot,
|
||||
bookingAttendees,
|
||||
ethSignature,
|
||||
duration,
|
||||
}) => {
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
const [slotPickerRef] = useAutoAnimate<HTMLDivElement>();
|
||||
const { t, i18n } = useLocale();
|
||||
const router = useRouter();
|
||||
@@ -63,6 +67,14 @@ const AvailableTimes: FC<AvailableTimesProps> = ({
|
||||
[isMobile]
|
||||
);
|
||||
|
||||
const reserveSlot = (slot: Slot) => {
|
||||
reserveSlotMutation.mutate({
|
||||
slotUtcStartDate: slot.time,
|
||||
eventTypeId,
|
||||
slotUtcEndDate: dayjs(slot.time).utc().add(duration, "minutes").format(),
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<div ref={slotPickerRef}>
|
||||
{!!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",
|
||||
brand === "#fff" || brand === "#ffffff" ? "" : ""
|
||||
)}
|
||||
onClick={() => reserveSlot(slot)}
|
||||
data-testid="time">
|
||||
{dayjs(slot.time).tz(timeZone()).format(timeFormat)}
|
||||
{!!seatsPerTimeSlot && (
|
||||
|
||||
@@ -6,7 +6,7 @@ import dayjs from "@calcom/dayjs";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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";
|
||||
|
||||
@@ -103,7 +103,7 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
{requiresConfirmation && (
|
||||
<div className={classNames("items-top flex", isBookingPage && "text-default text-sm font-medium")}>
|
||||
<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>
|
||||
{requiresConfirmationText}
|
||||
</div>
|
||||
@@ -117,9 +117,9 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
isBookingPage && "text-default",
|
||||
!eventType.metadata?.multipleDuration && "items-center"
|
||||
)}>
|
||||
<FiClock
|
||||
<Clock
|
||||
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]"
|
||||
)}
|
||||
/>
|
||||
@@ -132,9 +132,9 @@ const BookingDescription: FC<Props> = (props) => {
|
||||
variant="gray"
|
||||
className={classNames(
|
||||
duration === dur.toString()
|
||||
? "bg-darkgray-200 text-darkgray-900 dark:bg-darkmodebrand dark:!text-darkmodebrandcontrast"
|
||||
: "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",
|
||||
"cursor-pointer"
|
||||
? "bg-emphasis border-emphasis text-emphasis "
|
||||
: "bg-subtle text-default border-transparent ",
|
||||
"cursor-pointer border"
|
||||
)}
|
||||
onClick={() => {
|
||||
setDuration(dur);
|
||||
|
||||
@@ -1,22 +1,35 @@
|
||||
import type { TFunction } from "next-i18next";
|
||||
import { FormattedNumber, IntlProvider } from "react-intl";
|
||||
|
||||
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);
|
||||
if (!paymentAppData || paymentAppData.price <= 0) return null;
|
||||
|
||||
return (
|
||||
<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]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>
|
||||
{props.t("no_show_fee_amount", {
|
||||
amount: paymentAppData.price / 100.0,
|
||||
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)}
|
||||
</p>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -28,10 +28,11 @@ import {
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
} 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 { ChargeCardDialog } from "@components/dialog/ChargeCardDialog";
|
||||
import { EditLocationDialog } from "@components/dialog/EditLocationDialog";
|
||||
import { RescheduleDialog } from "@components/dialog/RescheduleDialog";
|
||||
|
||||
@@ -56,7 +57,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
const router = useRouter();
|
||||
const [rejectionReason, setRejectionReason] = useState<string>("");
|
||||
const [rejectionDialogIsOpen, setRejectionDialogIsOpen] = useState(false);
|
||||
const [chargeCardDialogIsOpen, setChargeCardDialogIsOpen] = useState(false);
|
||||
const [viewRecordingsDialogIsOpen, setViewRecordingsDialogIsOpen] = useState<boolean>(false);
|
||||
const cardCharged = booking?.payment[0]?.success;
|
||||
const mutation = trpc.viewer.bookings.confirm.useMutation({
|
||||
onSuccess: (data) => {
|
||||
if (data?.status === BookingStatus.REJECTED) {
|
||||
@@ -113,19 +116,23 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: FiSlash,
|
||||
icon: Slash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
{
|
||||
id: "confirm",
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: FiCheck,
|
||||
disabled: mutation.isLoading,
|
||||
color: "primary",
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
...((isPending && !booking?.eventType?.price) || (!!booking?.eventType?.price && booking.paid)
|
||||
? [
|
||||
{
|
||||
id: "confirm",
|
||||
label: (isTabRecurring || isTabUnconfirmed) && isRecurring ? t("confirm_all") : t("confirm"),
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: Check,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const showRecordingActions: ActionType[] = [
|
||||
@@ -149,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||
`,
|
||||
icon: FiX,
|
||||
icon: X,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
@@ -157,7 +164,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
actions: [
|
||||
{
|
||||
id: "reschedule",
|
||||
icon: FiClock,
|
||||
icon: Clock,
|
||||
label: t("reschedule_booking"),
|
||||
href: `/reschedule/${booking.uid}${
|
||||
booking.seatsReferences.length ? `?seatReferenceUid=${getSeatReferenceUid()}` : ""
|
||||
@@ -165,7 +172,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
{
|
||||
id: "reschedule_request",
|
||||
icon: FiSend,
|
||||
icon: Send,
|
||||
iconClassName: "rotate-45 w-[16px] -translate-x-0.5 ",
|
||||
label: t("send_reschedule_request"),
|
||||
onClick: () => {
|
||||
@@ -178,12 +185,24 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
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) {
|
||||
bookedActions = bookedActions.filter((action) => action.id !== "edit_booking");
|
||||
}
|
||||
@@ -194,7 +213,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
|
||||
const RequestSentMessage = () => {
|
||||
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")}
|
||||
</Badge>
|
||||
);
|
||||
@@ -237,10 +256,9 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
},
|
||||
});
|
||||
};
|
||||
const showRecordingsButtons =
|
||||
(booking.location === "integrations:daily" || booking?.location?.trim() === "") && isPast && isConfirmed;
|
||||
|
||||
const title = booking.title;
|
||||
const showRecordingsButtons = booking.isRecorded && isPast && isConfirmed;
|
||||
return (
|
||||
<>
|
||||
<RescheduleDialog
|
||||
@@ -254,6 +272,15 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isOpenDialog={isOpenSetLocationDialog}
|
||||
setShowLocationModal={setIsOpenLocationDialog}
|
||||
/>
|
||||
{booking.paid && (
|
||||
<ChargeCardDialog
|
||||
isOpenDialog={chargeCardDialogIsOpen}
|
||||
setIsOpenDialog={setChargeCardDialogIsOpen}
|
||||
bookingId={booking.id}
|
||||
paymentAmount={booking?.payment[0].amount}
|
||||
paymentCurrency={booking?.payment[0].currency}
|
||||
/>
|
||||
)}
|
||||
{showRecordingsButtons && (
|
||||
<ViewRecordingsDialog
|
||||
booking={booking}
|
||||
@@ -321,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
)}
|
||||
{booking.paid && (
|
||||
<Badge className="ltr:mr-2 rtl:ml-2" variant="green">
|
||||
{t("paid")}
|
||||
{booking.payment[0].paymentOption === "HOLD" ? t("card_held") : t("paid")}
|
||||
</Badge>
|
||||
)}
|
||||
{recurringDates !== undefined && (
|
||||
@@ -423,6 +450,11 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
<RequestSentMessage />
|
||||
</div>
|
||||
)}
|
||||
{booking.status === "ACCEPTED" && booking.paid && booking?.payment[0]?.paymentOption === "HOLD" && (
|
||||
<div className="ml-2">
|
||||
<TableActions actions={chargeCardActions} />
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
</tr>
|
||||
</>
|
||||
@@ -476,7 +508,7 @@ const RecurringBookingsTooltip = ({ booking, recurringDates }: RecurringBookings
|
||||
);
|
||||
})}>
|
||||
<div className="text-default">
|
||||
<FiRefreshCcw
|
||||
<RefreshCcw
|
||||
strokeWidth="3"
|
||||
className="text-muted float-left mr-1 mt-1.5 inline-block h-3 w-3"
|
||||
/>
|
||||
|
||||
@@ -6,7 +6,7 @@ import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Button, TextArea } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
|
||||
type Props = {
|
||||
booking: {
|
||||
@@ -48,7 +48,7 @@ export default function CancelBooking(props: Props) {
|
||||
{error && (
|
||||
<div className="mt-8">
|
||||
<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 className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -194,6 +194,7 @@ export const SlotPicker = ({
|
||||
bookingAttendees={bookingAttendees}
|
||||
recurringCount={recurringEventCount}
|
||||
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";
|
||||
|
||||
@@ -18,7 +18,7 @@ export function TimezoneDropdown({
|
||||
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">
|
||||
<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} />
|
||||
</div>
|
||||
</>
|
||||
|
||||
@@ -24,8 +24,8 @@ import { getRecurringFreq } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { detectBrowserTimeFormat, setIs24hClockInLocalStorage, TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { HeadSeo, useCalcomTheme } from "@calcom/ui";
|
||||
import { FiCreditCard, FiUser, FiRefreshCcw } from "@calcom/ui/components/icon";
|
||||
import { HeadSeo, NumberInput, useCalcomTheme } from "@calcom/ui";
|
||||
import { CreditCard, User, RefreshCcw } from "@calcom/ui/components/icon";
|
||||
|
||||
import { timeZone as localStorageTimeZone } from "@lib/clock";
|
||||
|
||||
@@ -124,6 +124,16 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
[timeZone]
|
||||
);
|
||||
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 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.
|
||||
@@ -181,7 +191,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
style={availabilityDatePickerEmbedStyles}
|
||||
className={classNames(
|
||||
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"
|
||||
)}>
|
||||
<div className="md:flex">
|
||||
@@ -200,7 +210,7 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
" text-default",
|
||||
"ltr:mr-[10px] rtl:ml-[10px]"
|
||||
)}>
|
||||
<FiUser
|
||||
<User
|
||||
className={classNames(
|
||||
"min-h-4 min-w-4 ml-[2px] inline-block ltr:mr-[10px] rtl:ml-[10px]",
|
||||
"mt-[2px]"
|
||||
@@ -211,21 +221,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
)}
|
||||
{!rescheduleUid && eventType.recurringEvent && (
|
||||
<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>
|
||||
<p className="mb-1 -ml-2 inline px-2 py-1">
|
||||
{getRecurringFreq({ t, recurringEvent: eventType.recurringEvent })}
|
||||
</p>
|
||||
<input
|
||||
type="number"
|
||||
|
||||
<NumberInput
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
min="1"
|
||||
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"
|
||||
defaultValue={eventType.recurringEvent.count}
|
||||
isFullWidth={false}
|
||||
className="me-2 inline w-16"
|
||||
onChange={(event) => {
|
||||
setRecurringEventCount(parseInt(event?.target.value));
|
||||
}}
|
||||
/>
|
||||
|
||||
<p className="inline">
|
||||
{t("occurrence", {
|
||||
count: recurringEventCount,
|
||||
@@ -236,14 +248,23 @@ const AvailabilityPage = ({ profile, eventType, ...restProps }: Props) => {
|
||||
)}
|
||||
{paymentAppData.price > 0 && (
|
||||
<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]" />
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
<CreditCard className="ml-[2px] -mt-1 inline-block h-4 w-4 ltr:mr-[10px] rtl:ml-[10px]" />
|
||||
{paymentAppData.paymentOption === "HOLD" ? (
|
||||
<>
|
||||
{t("no_show_fee_amount", {
|
||||
amount: paymentAppData.price / 100.0,
|
||||
formatParams: { amount: { currency: paymentAppData.currency } },
|
||||
})}
|
||||
</>
|
||||
) : (
|
||||
<IntlProvider locale="en">
|
||||
<FormattedNumber
|
||||
value={paymentAppData.price / 100.0}
|
||||
style="currency"
|
||||
currency={paymentAppData.currency?.toUpperCase()}
|
||||
/>
|
||||
</IntlProvider>
|
||||
)}
|
||||
</p>
|
||||
)}
|
||||
{timezoneDropdown}
|
||||
|
||||
@@ -32,7 +32,7 @@ import getLocationOptionsForSelect from "@calcom/features/bookings/lib/getLocati
|
||||
import { FormBuilderField } from "@calcom/features/form-builder/FormBuilder";
|
||||
import { bookingSuccessRedirect } from "@calcom/lib/bookingSuccessRedirect";
|
||||
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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||
@@ -41,8 +41,9 @@ import { HttpError } from "@calcom/lib/http-error";
|
||||
import { getEveryFreqFor } from "@calcom/lib/recurringStrings";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import { TimeFormat } from "@calcom/lib/timeFormat";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
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 useRouterQuery from "@lib/hooks/useRouterQuery";
|
||||
@@ -210,8 +211,11 @@ const BookingPage = ({
|
||||
hashedLink,
|
||||
...restProps
|
||||
}: BookingPageProps) => {
|
||||
const removeSelectedSlotMarkMutation = trpc.viewer.public.slots.removeSelectedSlotMark.useMutation();
|
||||
const reserveSlotMutation = trpc.viewer.public.slots.reserveSlot.useMutation();
|
||||
const { t, i18n } = useLocale();
|
||||
const { duration: queryDuration } = useRouterQuery("duration");
|
||||
const { date: queryDate } = useRouterQuery("date");
|
||||
const isEmbed = useIsEmbed(restProps.isEmbed);
|
||||
const embedUiConfig = useEmbedUiConfig();
|
||||
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
|
||||
let duration = eventType.length;
|
||||
if (
|
||||
@@ -246,6 +259,12 @@ const BookingPage = ({
|
||||
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
|
||||
}, []);
|
||||
|
||||
@@ -528,17 +547,17 @@ const BookingPage = ({
|
||||
<div
|
||||
className={classNames(
|
||||
"main",
|
||||
isBackgroundTransparent ? "" : "dark:bg-darkgray-100 bg-default dark:border",
|
||||
"dark:border-darkgray-300 rounded-md sm:border"
|
||||
isBackgroundTransparent ? "" : "bg-default",
|
||||
"border-booker sm:border-booker-width rounded-md"
|
||||
)}>
|
||||
<div className="sm:flex">
|
||||
{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}>
|
||||
<BookingDescriptionPayment eventType={eventType} />
|
||||
<BookingDescriptionPayment eventType={eventType} t={t} />
|
||||
{!rescheduleUid && eventType.recurringEvent?.freq && recurringEventCount && (
|
||||
<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">
|
||||
{getEveryFreqFor({
|
||||
t,
|
||||
@@ -549,7 +568,7 @@ const BookingPage = ({
|
||||
</div>
|
||||
)}
|
||||
<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">
|
||||
{isClientTimezoneAvailable &&
|
||||
(rescheduleUid || !eventType.recurringEvent?.freq) &&
|
||||
@@ -580,7 +599,7 @@ const BookingPage = ({
|
||||
{t("former_time")}
|
||||
</p>
|
||||
<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 &&
|
||||
typeof booking.startTime === "string" &&
|
||||
parseDate(dayjs(booking.startTime), i18n, timeFormat)}
|
||||
@@ -589,7 +608,7 @@ const BookingPage = ({
|
||||
)}
|
||||
{!!eventType.seatsPerTimeSlot && (
|
||||
<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] ${
|
||||
currentSlotBooking &&
|
||||
currentSlotBooking.attendees.length / eventType.seatsPerTimeSlot >= 0.5
|
||||
@@ -649,9 +668,9 @@ const BookingPage = ({
|
||||
</Button>
|
||||
</div>
|
||||
</Form>
|
||||
{(mutation.isError || recurringMutation.isError) && (
|
||||
{mutation.isError || recurringMutation.isError ? (
|
||||
<ErrorMessage error={mutation.error || recurringMutation.error} />
|
||||
)}
|
||||
) : null}
|
||||
</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 className="flex">
|
||||
<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 className="ms-3">
|
||||
<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 { Input } 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";
|
||||
|
||||
@@ -218,7 +218,7 @@ export const EditLocationDialog = (props: ISetLocationDialog) => {
|
||||
<DialogContent>
|
||||
<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">
|
||||
<FiMapPin className="text-emphasis h-6 w-6" />
|
||||
<MapPin className="text-emphasis h-6 w-6" />
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<div className="mt-3 text-center sm:mt-0 sm:text-left">
|
||||
|
||||
@@ -13,7 +13,7 @@ import {
|
||||
showToast,
|
||||
TextArea,
|
||||
} from "@calcom/ui";
|
||||
import { FiClock } from "@calcom/ui/components/icon";
|
||||
import { Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -44,7 +44,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
||||
<DialogContent>
|
||||
<div className="flex flex-row space-x-3">
|
||||
<div className="flex h-10 w-10 flex-shrink-0 justify-center rounded-full bg-[#FAFAFA]">
|
||||
<FiClock className="m-auto h-6 w-6" />
|
||||
<Clock className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("send_reschedule_request")} />
|
||||
|
||||
@@ -10,14 +10,15 @@ import type { EventNameObjectType } from "@calcom/core/event";
|
||||
import { getEventName } from "@calcom/core/event";
|
||||
import getLocationsOptionsForSelect from "@calcom/features/bookings/lib/getLocationOptionsForSelect";
|
||||
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 { classNames } from "@calcom/lib";
|
||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
import { Button, Checkbox, Label, SettingsToggle, showToast, TextField, Tooltip, Alert } from "@calcom/ui";
|
||||
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
@@ -58,6 +59,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||
const noShowFeeEnabled = eventType.metadata.apps?.stripe?.paymentOption === "HOLD";
|
||||
|
||||
useEffect(() => {
|
||||
!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({
|
||||
...eventNameObject,
|
||||
eventName: formMethods.watch("eventName"),
|
||||
});
|
||||
|
||||
const successRedirectUrlLocked = shouldLockDisableProps("successRedirectUrl");
|
||||
const seatsLocked = shouldLockDisableProps("seatsPerTimeSlotEnabled");
|
||||
|
||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||
|
||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
||||
@@ -127,19 +137,19 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<TextField
|
||||
label={t("event_name_in_calendar")}
|
||||
type="text"
|
||||
{...shouldLockDisableProps("eventName")}
|
||||
placeholder={eventNamePlaceholder}
|
||||
defaultValue={eventType.eventName || ""}
|
||||
{...formMethods.register("eventName")}
|
||||
addOnSuffix={
|
||||
<Button
|
||||
type="button"
|
||||
StartIcon={FiEdit}
|
||||
variant="icon"
|
||||
color="minimal"
|
||||
className="hover:stroke-3 hover:text-emphasis min-w-fit px-0 hover:bg-transparent"
|
||||
onClick={() => setShowEventNameTip((old) => !old)}
|
||||
size="sm"
|
||||
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>
|
||||
@@ -149,6 +159,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
description={t("booking_questions_description")}
|
||||
addFieldLabel={t("add_a_booking_question")}
|
||||
formProp="bookingFields"
|
||||
{...shouldLockDisableProps("bookingFields")}
|
||||
dataStore={{
|
||||
options: {
|
||||
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
||||
@@ -157,6 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
/>
|
||||
<hr className="border-subtle" />
|
||||
<RequiresConfirmationController
|
||||
eventType={eventType}
|
||||
seatsEnabled={seatsEnabled}
|
||||
metadata={eventType.metadata}
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
@@ -170,6 +182,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
title={t("disable_notes")}
|
||||
{...shouldLockDisableProps("hideCalendarNotes")}
|
||||
description={t("disable_notes_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => onChange(e)}
|
||||
@@ -184,6 +197,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<>
|
||||
<SettingsToggle
|
||||
title={t("redirect_success_booking")}
|
||||
{...successRedirectUrlLocked}
|
||||
description={t("redirect_url_description")}
|
||||
checked={redirectUrlVisible}
|
||||
onCheckedChange={(e) => {
|
||||
@@ -196,6 +210,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
className="w-full"
|
||||
label={t("redirect_success_booking")}
|
||||
labelSrOnly
|
||||
disabled={successRedirectUrlLocked.disabled}
|
||||
placeholder={t("external_redirect_url")}
|
||||
required={redirectUrlVisible}
|
||||
type="text"
|
||||
@@ -218,6 +233,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<SettingsToggle
|
||||
data-testid="hashedLinkCheck"
|
||||
title={t("private_link")}
|
||||
{...shouldLockDisableProps("hashedLinkCheck")}
|
||||
description={t("private_link_description", { appName: APP_NAME })}
|
||||
checked={hashedLinkVisible}
|
||||
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")}>
|
||||
<Button
|
||||
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={() => {
|
||||
navigator.clipboard.writeText(placeholderHashedLink);
|
||||
if (eventType.hashedLink) {
|
||||
@@ -246,10 +266,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
} else {
|
||||
showToast(t("enabled_after_update_description"), "warning");
|
||||
}
|
||||
}}
|
||||
className="hover:stroke-3 hover:text-emphasis hover:bg-transparent"
|
||||
type="button">
|
||||
<FiCopy />
|
||||
}}>
|
||||
<Copy className="h-4 w-4" />
|
||||
</Button>
|
||||
</Tooltip>
|
||||
}
|
||||
@@ -262,54 +280,62 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
control={formMethods.control}
|
||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
onCheckedChange={(e) => {
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
}
|
||||
onChange(e);
|
||||
}}>
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
labelSrOnly
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
defaultValue={value || 2}
|
||||
min={1}
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
/>
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
description={t("show_attendees")}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
<>
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
{...seatsLocked}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
disabled={noShowFeeEnabled}
|
||||
onCheckedChange={(e) => {
|
||||
// Enabling seats will disable guests and requiring confirmation until fully supported
|
||||
if (e) {
|
||||
toggleGuests(false);
|
||||
formMethods.setValue("requiresConfirmation", false);
|
||||
setRequiresConfirmation(false);
|
||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
||||
} else {
|
||||
formMethods.setValue("seatsPerTimeSlot", null);
|
||||
toggleGuests(true);
|
||||
}
|
||||
onChange(e);
|
||||
}}>
|
||||
<Controller
|
||||
name="seatsPerTimeSlot"
|
||||
control={formMethods.control}
|
||||
defaultValue={eventType.seatsPerTimeSlot}
|
||||
render={({ field: { value, onChange } }) => (
|
||||
<div className="lg:-ml-2">
|
||||
<TextField
|
||||
required
|
||||
name="seatsPerTimeSlot"
|
||||
labelSrOnly
|
||||
label={t("number_of_seats")}
|
||||
type="number"
|
||||
disabled={seatsLocked.disabled}
|
||||
defaultValue={value || 2}
|
||||
min={1}
|
||||
className="w-24"
|
||||
addOnSuffix={<>{t("seats")}</>}
|
||||
onChange={(e) => {
|
||||
onChange(Math.abs(Number(e.target.value)));
|
||||
}}
|
||||
/>
|
||||
<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>
|
||||
)}
|
||||
/>
|
||||
</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 type { EventTypeAppCardComponentProps } from "@calcom/app-store/types";
|
||||
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 { trpc } from "@calcom/trpc/react";
|
||||
import { Button, EmptyScreen } from "@calcom/ui";
|
||||
import { FiGrid } from "@calcom/ui/components/icon";
|
||||
import { Button, EmptyScreen, Alert } from "@calcom/ui";
|
||||
import { Grid, Lock } from "@calcom/ui/components/icon";
|
||||
|
||||
export type EventType = Pick<EventTypeSetupProps, "eventType">["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 (
|
||||
<>
|
||||
<div>
|
||||
<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 ? (
|
||||
<EmptyScreen
|
||||
Icon={FiGrid}
|
||||
Icon={Grid}
|
||||
headline={t("empty_installed_apps_headline")}
|
||||
description={t("empty_installed_apps_description")}
|
||||
buttonRaw={
|
||||
<Button target="_blank" color="secondary" href="/apps">
|
||||
{t("empty_installed_apps_button")}{" "}
|
||||
</Button>
|
||||
isChildrenManagedEventType && !isManagedEventType ? (
|
||||
<Button StartIcon={Lock} color="secondary" disabled>
|
||||
{t("locked_by_admin")}
|
||||
</Button>
|
||||
) : (
|
||||
<Button target="_blank" color="secondary" href="/apps">
|
||||
{t("empty_installed_apps_button")}{" "}
|
||||
</Button>
|
||||
)
|
||||
}
|
||||
/>
|
||||
) : null}
|
||||
@@ -82,22 +103,24 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
{!isLoading && notInstalledApps?.length ? (
|
||||
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">Available Apps</h2>
|
||||
) : null}
|
||||
<div className="before:border-0">
|
||||
{notInstalledApps?.map((app) => (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
/>
|
||||
))}
|
||||
{!shouldLockDisableProps("apps").disabled && (
|
||||
<div>
|
||||
{!isLoading && notInstalledApps?.length ? (
|
||||
<h2 className="text-emphasis mt-0 mb-2 text-lg font-semibold">{t("available_apps")}</h2>
|
||||
) : null}
|
||||
<div className="before:border-0">
|
||||
{notInstalledApps?.map((app) => (
|
||||
<EventTypeAppCard
|
||||
getAppData={getAppDataGetter(app.slug as EventTypeAppsList)}
|
||||
setAppData={getAppDataSetter(app.slug as EventTypeAppsList)}
|
||||
key={app.slug}
|
||||
app={app}
|
||||
eventType={eventType}
|
||||
/>
|
||||
))}
|
||||
</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 type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
import { ExternalLink, Globe, Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
type AvailabilityOption = {
|
||||
label: string;
|
||||
value: number;
|
||||
isDefault: boolean;
|
||||
isManaged?: boolean;
|
||||
};
|
||||
|
||||
const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||
const { label, isDefault } = props.data;
|
||||
const { label, isDefault, isManaged = false } = props.data;
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<components.Option {...props}>
|
||||
@@ -33,12 +33,17 @@ const Option = ({ ...props }: OptionProps<AvailabilityOption>) => {
|
||||
{t("default")}
|
||||
</Badge>
|
||||
)}
|
||||
{isManaged && (
|
||||
<Badge variant="gray" className="ml-2">
|
||||
{t("managed")}
|
||||
</Badge>
|
||||
)}
|
||||
</components.Option>
|
||||
);
|
||||
};
|
||||
|
||||
const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||
const { label, isDefault } = props.data;
|
||||
const { label, isDefault, isManaged = false } = props.data;
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<components.SingleValue {...props}>
|
||||
@@ -48,50 +53,39 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||
{t("default")}
|
||||
</Badge>
|
||||
)}
|
||||
{isManaged && (
|
||||
<Badge variant="gray" className="ml-2">
|
||||
{t("managed")}
|
||||
</Badge>
|
||||
)}
|
||||
</components.SingleValue>
|
||||
);
|
||||
};
|
||||
|
||||
const AvailabilitySelect = ({
|
||||
className = "",
|
||||
isLoading,
|
||||
schedules,
|
||||
options,
|
||||
value,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
isLoading: boolean;
|
||||
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||
value: AvailabilityOption | undefined;
|
||||
options: AvailabilityOption[];
|
||||
isDisabled?: boolean;
|
||||
onBlur: () => void;
|
||||
onChange: (value: AvailabilityOption | null) => void;
|
||||
}) => {
|
||||
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 (
|
||||
<Select
|
||||
placeholder={t("select")}
|
||||
options={options}
|
||||
isDisabled={props.isDisabled}
|
||||
isSearchable={false}
|
||||
onChange={props.onChange}
|
||||
className={classNames("block w-full min-w-0 flex-1 rounded-sm text-sm", className)}
|
||||
value={value}
|
||||
defaultValue={value}
|
||||
components={{ Option, SingleValue }}
|
||||
isMulti={false}
|
||||
/>
|
||||
@@ -103,17 +97,25 @@ const format = (date: Date, hour12: boolean) =>
|
||||
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 timeFormat = loggedInUser?.timeFormat;
|
||||
const { t, i18n } = useLocale();
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
|
||||
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) =>
|
||||
@@ -157,15 +159,15 @@ const EventTypeScheduleDetails = () => {
|
||||
<hr className="border-subtle" />
|
||||
<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">
|
||||
<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" />}
|
||||
</span>
|
||||
{!!schedule?.id && (
|
||||
{!!schedule?.id && !schedule.isManaged && (
|
||||
<Button
|
||||
href={`/availability/${schedule.id}`}
|
||||
disabled={isLoading}
|
||||
color="minimal"
|
||||
EndIcon={FiExternalLink}
|
||||
EndIcon={ExternalLink}
|
||||
target="_blank"
|
||||
rel="noopener noreferrer">
|
||||
{t("edit_availability")}
|
||||
@@ -176,14 +178,23 @@ const EventTypeScheduleDetails = () => {
|
||||
);
|
||||
};
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
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 (
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
Icon={Clock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
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 (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<label htmlFor="availability" className="text-default mb-2 block text-sm font-medium leading-none">
|
||||
{t("availability")}
|
||||
{shouldLockIndicator("availability")}
|
||||
</label>
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<>
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
value={value}
|
||||
options={options}
|
||||
onBlur={field.onBlur}
|
||||
isDisabled={shouldLockDisableProps("schedule").disabled}
|
||||
name={field.name}
|
||||
schedules={schedules?.schedules || []}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
@@ -215,12 +269,21 @@ const EventTypeSchedule = () => {
|
||||
)}
|
||||
/>
|
||||
</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>
|
||||
);
|
||||
};
|
||||
|
||||
const UseCommonScheduleSettingsToggle = () => {
|
||||
const UseCommonScheduleSettingsToggle = ({ eventType }: { eventType: EventTypeSetup }) => {
|
||||
const { t } = useLocale();
|
||||
const { resetField, setValue } = useFormContext<FormValues>();
|
||||
return (
|
||||
@@ -239,13 +302,23 @@ const UseCommonScheduleSettingsToggle = () => {
|
||||
}}
|
||||
title={t("choose_common_schedule_team_event")}
|
||||
description={t("choose_common_schedule_team_event_description")}>
|
||||
<EventTypeSchedule />
|
||||
<EventTypeSchedule eventType={eventType} />
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export const AvailabilityTab = ({ isTeamEvent }: { isTeamEvent: boolean }) => {
|
||||
return isTeamEvent ? <UseCommonScheduleSettingsToggle /> : <EventTypeSchedule />;
|
||||
export const EventAvailabilityTab = ({
|
||||
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 type { SingleValue } from "react-select";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { DurationType } 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 type { PeriodType } from "@calcom/prisma/client";
|
||||
import type { IntervalLimit } from "@calcom/types/Calendar";
|
||||
import {
|
||||
Button,
|
||||
DateRangePicker,
|
||||
Input,
|
||||
InputField,
|
||||
Label,
|
||||
Select,
|
||||
SettingsToggle,
|
||||
TextField,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus, FiTrash } from "@calcom/ui/components/icon";
|
||||
import { Button, DateRangePicker, InputField, Label, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||
import { Plus, Trash } from "@calcom/ui/components/icon";
|
||||
|
||||
const MinimumBookingNoticeInput = React.forwardRef<
|
||||
HTMLInputElement,
|
||||
@@ -78,6 +70,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
<div className="w-1/2 md:w-full">
|
||||
<InputField
|
||||
required
|
||||
disabled={passThroughProps.disabled}
|
||||
defaultValue={minimumBookingNoticeDisplayValues.value}
|
||||
onChange={(e) =>
|
||||
setMinimumBookingNoticeDisplayValues({
|
||||
@@ -95,6 +88,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
||||
</div>
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={passThroughProps.disabled}
|
||||
className="mb-0 ml-2 h-[38px] w-full capitalize md:min-w-[150px] md:max-w-[200px]"
|
||||
defaultValue={durationTypeOptions.find(
|
||||
(option) => option.value === minimumBookingNoticeDisplayValues.type
|
||||
@@ -146,12 +140,30 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
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 (
|
||||
<div className="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="w-full">
|
||||
<Label htmlFor="beforeBufferTime">{t("before_event")} </Label>
|
||||
<Label htmlFor="beforeBufferTime">
|
||||
{t("before_event")}
|
||||
{shouldLockIndicator("bookingLimits")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="beforeBufferTime"
|
||||
control={formMethods.control}
|
||||
@@ -170,6 +182,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
@@ -183,7 +196,10 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="afterBufferTime">{t("after_event")} </Label>
|
||||
<Label htmlFor="afterBufferTime">
|
||||
{t("after_event")}
|
||||
{shouldLockIndicator("bookingLimits")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="afterBufferTime"
|
||||
control={formMethods.control}
|
||||
@@ -202,6 +218,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={shouldLockDisableProps("bookingLimits").disabled}
|
||||
onChange={(val) => {
|
||||
if (val) onChange(val.value);
|
||||
}}
|
||||
@@ -217,11 +234,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
</div>
|
||||
<div className="flex flex-col space-y-4 lg:flex-row lg:space-y-0 lg:space-x-4">
|
||||
<div className="w-full">
|
||||
<Label htmlFor="minimumBookingNotice">{t("minimum_booking_notice")} </Label>
|
||||
<MinimumBookingNoticeInput {...formMethods.register("minimumBookingNotice")} />
|
||||
<Label htmlFor="minimumBookingNotice">
|
||||
{t("minimum_booking_notice")}
|
||||
{shouldLockIndicator("minimumBookingNotice")}
|
||||
</Label>
|
||||
<MinimumBookingNoticeInput
|
||||
disabled={shouldLockDisableProps("minimumBookingNotice").disabled}
|
||||
{...formMethods.register("minimumBookingNotice")}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-full">
|
||||
<Label htmlFor="slotInterval">{t("slot_interval")} </Label>
|
||||
<Label htmlFor="slotInterval">
|
||||
{t("slot_interval")}
|
||||
{shouldLockIndicator("slotInterval")}
|
||||
</Label>
|
||||
<Controller
|
||||
name="slotInterval"
|
||||
control={formMethods.control}
|
||||
@@ -239,6 +265,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
return (
|
||||
<Select
|
||||
isSearchable={false}
|
||||
isDisabled={shouldLockDisableProps("slotInterval").disabled}
|
||||
onChange={(val) => {
|
||||
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 } }) => (
|
||||
<SettingsToggle
|
||||
title={t("limit_booking_frequency")}
|
||||
{...bookingLimitsLocked}
|
||||
description={t("limit_booking_frequency_description")}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
@@ -272,7 +300,12 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
formMethods.setValue("bookingLimits", {});
|
||||
}
|
||||
}}>
|
||||
<IntervalLimitsManager propertyName="bookingLimits" defaultLimit={1} step={1} />
|
||||
<IntervalLimitsManager
|
||||
disabled={bookingLimitsLocked.disabled}
|
||||
propertyName="bookingLimits"
|
||||
defaultLimit={1}
|
||||
step={1}
|
||||
/>
|
||||
</SettingsToggle>
|
||||
)}
|
||||
/>
|
||||
@@ -284,6 +317,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<SettingsToggle
|
||||
title={t("limit_total_booking_duration")}
|
||||
description={t("limit_total_booking_duration_description")}
|
||||
{...durationLimitsLocked}
|
||||
checked={Object.keys(value ?? {}).length > 0}
|
||||
onCheckedChange={(active) => {
|
||||
if (active) {
|
||||
@@ -297,6 +331,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<IntervalLimitsManager
|
||||
propertyName="durationLimits"
|
||||
defaultLimit={60}
|
||||
disabled={durationLimitsLocked.disabled}
|
||||
step={15}
|
||||
textFieldSuffix={t("minutes")}
|
||||
/>
|
||||
@@ -311,13 +346,16 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<SettingsToggle
|
||||
title={t("limit_future_bookings")}
|
||||
description={t("limit_future_bookings_description")}
|
||||
{...periodTypeLocked}
|
||||
checked={value && value !== "UNLIMITED"}
|
||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||
<RadioGroup.Root
|
||||
defaultValue={watchPeriodType}
|
||||
value={watchPeriodType}
|
||||
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;
|
||||
return (
|
||||
<div
|
||||
@@ -326,30 +364,42 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
watchPeriodType === "UNLIMITED" && "pointer-events-none opacity-30"
|
||||
)}
|
||||
key={period.type}>
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={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">
|
||||
<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>
|
||||
{!periodTypeLocked.disabled && (
|
||||
<RadioGroup.Item
|
||||
id={period.type}
|
||||
value={period.type}
|
||||
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.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.type === "ROLLING" && (
|
||||
<div className="flex h-9">
|
||||
<Input
|
||||
<div className="flex items-center">
|
||||
<TextField
|
||||
labelSrOnly
|
||||
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"
|
||||
disabled={periodTypeLocked.disabled}
|
||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||
defaultValue={eventType.periodDays || 30}
|
||||
/>
|
||||
<select
|
||||
id=""
|
||||
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"
|
||||
{...formMethods.register("periodCountCalendarDays")}
|
||||
defaultValue={eventType.periodCountCalendarDays ? "1" : "0"}>
|
||||
<option value="1">{t("calendar_days")}</option>
|
||||
<option value="0">{t("business_days")}</option>
|
||||
</select>
|
||||
<Select
|
||||
options={optionsPeriod}
|
||||
isSearchable={false}
|
||||
isDisabled={periodTypeLocked.disabled}
|
||||
onChange={(opt) => {
|
||||
formMethods.setValue(
|
||||
"periodCountCalendarDays",
|
||||
opt?.value.toString() as "0" | "1"
|
||||
);
|
||||
}}
|
||||
defaultValue={
|
||||
optionsPeriod.find(
|
||||
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
||||
) ?? optionsPeriod[0]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{period.type === "RANGE" && (
|
||||
@@ -362,6 +412,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
||||
<DateRangePicker
|
||||
startDate={formMethods.getValues("periodDates").startDate}
|
||||
endDate={formMethods.getValues("periodDates").endDate}
|
||||
disabled={periodTypeLocked.disabled}
|
||||
onDatesChange={({ startDate, endDate }) => {
|
||||
formMethods.setValue("periodDates", {
|
||||
startDate,
|
||||
@@ -400,6 +451,7 @@ type IntervalLimitItemProps = {
|
||||
step: number;
|
||||
value: number;
|
||||
textFieldSuffix?: string;
|
||||
disabled?: boolean;
|
||||
selectOptions: { value: keyof IntervalLimit; label: string }[];
|
||||
hasDeleteButton?: boolean;
|
||||
onDelete: (intervalLimitsKey: IntervalLimitsKey) => void;
|
||||
@@ -414,6 +466,7 @@ const IntervalLimitItem = ({
|
||||
textFieldSuffix,
|
||||
selectOptions,
|
||||
hasDeleteButton,
|
||||
disabled,
|
||||
onDelete,
|
||||
onLimitChange,
|
||||
onIntervalSelect,
|
||||
@@ -424,8 +477,9 @@ const IntervalLimitItem = ({
|
||||
required
|
||||
type="number"
|
||||
containerClassName={textFieldSuffix ? "w-44 -mb-1" : "w-16 mb-0"}
|
||||
className="mb-0"
|
||||
className="mb-0 !h-auto"
|
||||
placeholder={`${value}`}
|
||||
disabled={disabled}
|
||||
min={step}
|
||||
step={step}
|
||||
defaultValue={value}
|
||||
@@ -435,11 +489,12 @@ const IntervalLimitItem = ({
|
||||
<Select
|
||||
options={selectOptions}
|
||||
isSearchable={false}
|
||||
isDisabled={disabled}
|
||||
defaultValue={INTERVAL_LIMIT_OPTIONS.find((option) => option.value === limitKey)}
|
||||
onChange={onIntervalSelect}
|
||||
/>
|
||||
{hasDeleteButton && (
|
||||
<Button variant="icon" StartIcon={FiTrash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||
{hasDeleteButton && !disabled && (
|
||||
<Button variant="icon" StartIcon={Trash} color="destructive" onClick={() => onDelete(limitKey)} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
@@ -450,6 +505,7 @@ type IntervalLimitsManagerProps<K extends "durationLimits" | "bookingLimits"> =
|
||||
defaultLimit: number;
|
||||
step: number;
|
||||
textFieldSuffix?: string;
|
||||
disabled?: boolean;
|
||||
};
|
||||
|
||||
const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
@@ -457,6 +513,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
defaultLimit,
|
||||
step,
|
||||
textFieldSuffix,
|
||||
disabled,
|
||||
}: IntervalLimitsManagerProps<K>) => {
|
||||
const { watch, setValue, control } = useFormContext<FormValues>();
|
||||
const watchIntervalLimits = watch(propertyName);
|
||||
@@ -506,6 +563,7 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
limitKey={limitKey}
|
||||
step={step}
|
||||
value={value}
|
||||
disabled={disabled}
|
||||
textFieldSuffix={textFieldSuffix}
|
||||
hasDeleteButton={Object.keys(currentIntervalLimits).length > 1}
|
||||
selectOptions={INTERVAL_LIMIT_OPTIONS.filter(
|
||||
@@ -536,8 +594,8 @@ const IntervalLimitsManager = <K extends "durationLimits" | "bookingLimits">({
|
||||
/>
|
||||
);
|
||||
})}
|
||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && (
|
||||
<Button color="minimal" StartIcon={FiPlus} onClick={addLimit}>
|
||||
{currentIntervalLimits && Object.keys(currentIntervalLimits).length <= 3 && !disabled && (
|
||||
<Button color="minimal" StartIcon={Plus} onClick={addLimit}>
|
||||
{t("add_limit")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -11,7 +11,7 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
||||
|
||||
return (
|
||||
<div className="">
|
||||
<RecurringEventController paymentEnabled={requirePayment} recurringEvent={eventType.recurringEvent} />
|
||||
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -11,6 +11,7 @@ import { z } from "zod";
|
||||
|
||||
import type { EventLocationType } 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 { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { md } from "@calcom/lib/markdownIt";
|
||||
@@ -27,7 +28,7 @@ import {
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} 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 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 {
|
||||
description?: string | null;
|
||||
editable?: boolean;
|
||||
}
|
||||
|
||||
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||
@@ -64,6 +90,7 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||
excludedToolbarItems={["blockType"]}
|
||||
placeholder={t("quick_video_meeting")}
|
||||
editable={props.editable}
|
||||
/>
|
||||
) : (
|
||||
<SkeletonContainer>
|
||||
@@ -174,6 +201,13 @@ export const EventSetupTab = (
|
||||
resolver: zodResolver(locationFormSchema),
|
||||
});
|
||||
|
||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||
useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
const Locations = () => {
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -188,6 +222,12 @@ export const EventSetupTab = (
|
||||
return true;
|
||||
});
|
||||
|
||||
const defaultValue = isManagedEventType
|
||||
? locationOptions.find((op) => op.label === t("default"))?.options[0]
|
||||
: undefined;
|
||||
|
||||
const { locationDetails, locationAvailable } = getLocationInfo(props);
|
||||
|
||||
return (
|
||||
<div className="w-full">
|
||||
{validLocations.length === 0 && (
|
||||
@@ -195,6 +235,8 @@ export const EventSetupTab = (
|
||||
<LocationSelect
|
||||
placeholder={t("select")}
|
||||
options={locationOptions}
|
||||
isDisabled={shouldLockDisableProps("locations").disabled}
|
||||
defaultValue={defaultValue}
|
||||
isSearchable={false}
|
||||
className="block w-full min-w-0 flex-1 rounded-sm text-sm"
|
||||
menuPlacement="auto"
|
||||
@@ -253,10 +295,10 @@ export const EventSetupTab = (
|
||||
}}
|
||||
aria-label={t("edit")}
|
||||
className="hover:text-emphasis text-subtle mr-1 p-1">
|
||||
<FiEdit2 className="h-4 w-4" />
|
||||
<Edit2 className="h-4 w-4" />
|
||||
</button>
|
||||
<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>
|
||||
</div>
|
||||
</div>
|
||||
@@ -268,7 +310,7 @@ export const EventSetupTab = (
|
||||
location.type === MeetLocationType && destinationCalendar?.integration !== "google_calendar"
|
||||
) && (
|
||||
<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">
|
||||
<p>
|
||||
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>
|
||||
</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>
|
||||
<Button
|
||||
data-testid="add-location"
|
||||
StartIcon={FiPlus}
|
||||
StartIcon={Plus}
|
||||
color="minimal"
|
||||
onClick={() => setShowLocationModal(true)}>
|
||||
{t("add_location")}
|
||||
@@ -299,22 +349,33 @@ export const EventSetupTab = (
|
||||
);
|
||||
};
|
||||
|
||||
const lengthLockedProps = shouldLockDisableProps("length");
|
||||
const descriptionLockedProps = shouldLockDisableProps("description");
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div className="space-y-8">
|
||||
<TextField
|
||||
required
|
||||
label={t("title")}
|
||||
{...shouldLockDisableProps("title")}
|
||||
defaultValue={eventType.title}
|
||||
{...formMethods.register("title")}
|
||||
/>
|
||||
<div>
|
||||
<Label>{t("description")}</Label>
|
||||
<DescriptionEditor description={eventType?.description} />
|
||||
<Label>
|
||||
{t("description")}
|
||||
{shouldLockIndicator("description")}
|
||||
</Label>
|
||||
<DescriptionEditor
|
||||
description={eventType?.description}
|
||||
editable={!descriptionLockedProps.disabled}
|
||||
/>
|
||||
</div>
|
||||
<TextField
|
||||
required
|
||||
label={t("URL")}
|
||||
{...shouldLockDisableProps("slug")}
|
||||
defaultValue={eventType.slug}
|
||||
addOnLeading={
|
||||
<>
|
||||
@@ -367,12 +428,14 @@ export const EventSetupTab = (
|
||||
<div>
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
{t("default_duration")}
|
||||
{shouldLockIndicator("length")}
|
||||
</Skeleton>
|
||||
<Select
|
||||
value={defaultDuration}
|
||||
isSearchable={false}
|
||||
name="length"
|
||||
className="text-sm"
|
||||
isDisabled={lengthLockedProps.disabled}
|
||||
noOptionsMessage={() => t("default_duration_no_options")}
|
||||
options={selectedMultipleDuration}
|
||||
onChange={(option) => {
|
||||
@@ -388,32 +451,36 @@ export const EventSetupTab = (
|
||||
<TextField
|
||||
required
|
||||
type="number"
|
||||
{...lengthLockedProps}
|
||||
label={t("duration")}
|
||||
defaultValue={eventType.length ?? 15}
|
||||
{...formMethods.register("length")}
|
||||
addOnSuffix={<>{t("minutes")}</>}
|
||||
/>
|
||||
)}
|
||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||
<SettingsToggle
|
||||
title={t("allow_booker_to_select_duration")}
|
||||
checked={multipleDuration !== undefined}
|
||||
onCheckedChange={() => {
|
||||
if (multipleDuration !== undefined) {
|
||||
setMultipleDuration(undefined);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!lengthLockedProps.disabled && (
|
||||
<div className="!mt-4 [&_label]:my-1 [&_label]:font-normal">
|
||||
<SettingsToggle
|
||||
title={t("allow_booker_to_select_duration")}
|
||||
checked={multipleDuration !== undefined}
|
||||
onCheckedChange={() => {
|
||||
if (multipleDuration !== undefined) {
|
||||
setMultipleDuration(undefined);
|
||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||
formMethods.setValue("length", eventType.length);
|
||||
} else {
|
||||
setMultipleDuration([]);
|
||||
formMethods.setValue("metadata.multipleDuration", []);
|
||||
formMethods.setValue("length", 0);
|
||||
}
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<Skeleton as={Label} loadingClassName="w-16">
|
||||
{t("location")}
|
||||
{shouldLockIndicator("locations")}
|
||||
</Skeleton>
|
||||
|
||||
<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 { useEffect, useRef } 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 CheckedTeamSelect from "@calcom/features/eventtypes/components/CheckedTeamSelect";
|
||||
import ChildrenEventTypeSelect from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Label, Select } from "@calcom/ui";
|
||||
|
||||
interface IMemberToValue {
|
||||
interface IUserToValue {
|
||||
id: number | null;
|
||||
name: string | null;
|
||||
username: string | null;
|
||||
email: string;
|
||||
}
|
||||
|
||||
const mapUserToValue = ({ id, name, username, email }: IMemberToValue) => ({
|
||||
const mapUserToValue = ({ id, name, username, email }: IUserToValue) => ({
|
||||
value: `${id || ""}`,
|
||||
label: `${name || ""}`,
|
||||
avatar: `${WEBAPP_URL}/${username}/avatar.png`,
|
||||
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>) => {
|
||||
if (a.label < b.label) {
|
||||
return -1;
|
||||
@@ -35,6 +57,40 @@ const sortByLabel = (a: ReturnType<typeof mapUserToValue>, b: ReturnType<typeof
|
||||
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 = ({
|
||||
labelText,
|
||||
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 = ({
|
||||
teamMembers,
|
||||
}: {
|
||||
@@ -189,6 +260,7 @@ const Hosts = ({
|
||||
/>*/}
|
||||
</>
|
||||
),
|
||||
MANAGED: <></>,
|
||||
};
|
||||
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 schedulingTypeOptions: {
|
||||
@@ -216,9 +292,13 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
},
|
||||
];
|
||||
const teamMembersOptions = teamMembers.map(mapUserToValue);
|
||||
const childrenEventTypeOptions = teamMembers.map((member) => {
|
||||
return mapMemberToChildrenOption(member, eventType.slug);
|
||||
});
|
||||
const isManagedEventType = eventType.schedulingType === SchedulingType.MANAGED;
|
||||
return (
|
||||
<div>
|
||||
{team && (
|
||||
{team && !isManagedEventType && (
|
||||
<div className="space-y-5">
|
||||
<div className="flex flex-col">
|
||||
<Label>{t("scheduling_type")}</Label>
|
||||
@@ -239,6 +319,9 @@ export const EventTeamTab = ({ team, teamMembers }: Pick<EventTypeSetupProps, "t
|
||||
<Hosts teamMembers={teamMembersOptions} />
|
||||
</div>
|
||||
)}
|
||||
{team && isManagedEventType && (
|
||||
<ChildrenEventTypes childrenEventTypeOptions={childrenEventTypeOptions} />
|
||||
)}
|
||||
</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 { 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 type { WebhookFormSubmitData } from "@calcom/features/webhooks/components/WebhookForm";
|
||||
import WebhookListItem from "@calcom/features/webhooks/components/WebhookListItem";
|
||||
import { APP_NAME } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { Webhook } from "@calcom/prisma/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
import { Alert, Button, Dialog, DialogContent, EmptyScreen, showToast } from "@calcom/ui";
|
||||
import { Plus, Lock } from "@calcom/ui/components/icon";
|
||||
|
||||
export const EventTeamWebhooksTab = ({
|
||||
eventType,
|
||||
@@ -85,12 +86,20 @@ export const EventTeamWebhooksTab = ({
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="new_webhook"
|
||||
StartIcon={FiPlus}
|
||||
StartIcon={Plus}
|
||||
onClick={() => setCreateModalOpen(true)}>
|
||||
{t("new_webhook")}
|
||||
</Button>
|
||||
);
|
||||
};
|
||||
|
||||
const { shouldLockDisableProps, isChildrenManagedEventType, isManagedEventType } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
const webhookLockedStatus = shouldLockDisableProps("webhooks");
|
||||
|
||||
return (
|
||||
<div>
|
||||
{team && webhooks && !isLoading && (
|
||||
@@ -98,15 +107,24 @@ export const EventTeamWebhooksTab = ({
|
||||
<div>
|
||||
<div>
|
||||
<>
|
||||
{isManagedEventType && (
|
||||
<Alert
|
||||
severity="neutral"
|
||||
className="mb-2"
|
||||
title={t("locked_for_members")}
|
||||
message={t("locked_webhooks_description")}
|
||||
/>
|
||||
)}
|
||||
{webhooks.length ? (
|
||||
<>
|
||||
<div className="mb-8 rounded-md border">
|
||||
<div className="mb-2 rounded-md border">
|
||||
{webhooks.map((webhook, index) => {
|
||||
return (
|
||||
<WebhookListItem
|
||||
key={webhook.id}
|
||||
webhook={webhook}
|
||||
lastItem={webhooks.length === index + 1}
|
||||
canEditWebhook={!webhookLockedStatus.disabled}
|
||||
onEditWebhook={() => {
|
||||
setEditModalOpen(true);
|
||||
setWebhookToEdit(webhook);
|
||||
@@ -122,7 +140,15 @@ export const EventTeamWebhooksTab = ({
|
||||
Icon={TbWebhook}
|
||||
headline={t("create_your_first_webhook")}
|
||||
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 { Trans } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
||||
import { useMemo, useState, Suspense } from "react";
|
||||
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 { classNames } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
@@ -32,19 +35,19 @@ import {
|
||||
VerticalTabs,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiLink,
|
||||
FiCalendar,
|
||||
FiClock,
|
||||
FiSliders,
|
||||
FiRepeat,
|
||||
FiGrid,
|
||||
FiZap,
|
||||
FiUsers,
|
||||
FiExternalLink,
|
||||
FiCode,
|
||||
FiTrash,
|
||||
FiMoreHorizontal,
|
||||
FiLoader,
|
||||
Link as LinkIcon,
|
||||
Calendar,
|
||||
Clock,
|
||||
Sliders,
|
||||
Repeat,
|
||||
Grid,
|
||||
Zap,
|
||||
Users,
|
||||
ExternalLink,
|
||||
Code,
|
||||
Trash,
|
||||
MoreHorizontal,
|
||||
Loader,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { EmbedButton, EmbedDialog } from "@components/Embed";
|
||||
@@ -77,44 +80,38 @@ function getNavigation(props: {
|
||||
{
|
||||
name: "event_setup_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
||||
icon: FiLink,
|
||||
icon: LinkIcon,
|
||||
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",
|
||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
||||
icon: FiClock,
|
||||
icon: Clock,
|
||||
info: `event_limit_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "event_advanced_tab_title",
|
||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
||||
icon: FiSliders,
|
||||
icon: Sliders,
|
||||
info: `event_advanced_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "recurring",
|
||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
||||
icon: FiRepeat,
|
||||
icon: Repeat,
|
||||
info: `recurring_event_tab_description`,
|
||||
},
|
||||
{
|
||||
name: "apps",
|
||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
||||
icon: FiGrid,
|
||||
icon: Grid,
|
||||
//TODO: Handle proper translation with count handling
|
||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||
},
|
||||
{
|
||||
name: "workflows",
|
||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
||||
icon: FiZap,
|
||||
icon: Zap,
|
||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||
},
|
||||
];
|
||||
@@ -137,7 +134,10 @@ function EventTypeSingleLayout({
|
||||
const router = useRouter();
|
||||
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({
|
||||
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
|
||||
const EventTypeTabs = useMemo(() => {
|
||||
const navigation = getNavigation({
|
||||
let navigation = getNavigation({
|
||||
t,
|
||||
eventType,
|
||||
enabledAppsNumber,
|
||||
installedAppsNumber,
|
||||
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 (team) {
|
||||
navigation.splice(2, 0, {
|
||||
name: "assignment",
|
||||
href: `/event-types/${eventType.id}?tabName=team`,
|
||||
icon: FiUsers,
|
||||
info: eventType.schedulingType === "COLLECTIVE" ? "collective" : "round_robin",
|
||||
icon: Users,
|
||||
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({
|
||||
name: "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 isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
||||
|
||||
return (
|
||||
<Shell
|
||||
@@ -197,77 +230,87 @@ function EventTypeSingleLayout({
|
||||
heading={eventType.title}
|
||||
CTA={
|
||||
<div className="flex items-center justify-end">
|
||||
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||
{t("hide_from_profile")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
checked={formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<VerticalDivider className="hidden lg:block" />
|
||||
{!eventType.metadata.managedEventConfig && (
|
||||
<>
|
||||
<div className="sm:hover:bg-subtle hidden items-center rounded-md px-2 lg:flex">
|
||||
<Skeleton
|
||||
as={Label}
|
||||
htmlFor="hiddenSwitch"
|
||||
className="mt-2 hidden cursor-pointer self-center whitespace-nowrap pr-2 sm:inline">
|
||||
{t("hide_from_profile")}
|
||||
</Skeleton>
|
||||
<Switch
|
||||
id="hiddenSwitch"
|
||||
checked={formMethods.watch("hidden")}
|
||||
onCheckedChange={(e) => {
|
||||
formMethods.setValue("hidden", e);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
<VerticalDivider className="hidden lg:block" />
|
||||
</>
|
||||
)}
|
||||
|
||||
{/* TODO: Figure out why combined isnt working - works in storybook */}
|
||||
<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 */}
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="preview-button"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
StartIcon={FiExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
{/* We have to warp this in tooltip as it has a href which disabels the tooltip on buttons */}
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
data-testid="preview-button"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={permalink}
|
||||
rel="noreferrer"
|
||||
StartIcon={ExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={FiLink}
|
||||
tooltip={t("copy_link")}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
}}
|
||||
/>
|
||||
<EmbedButton
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
StartIcon={FiCode}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
<Button
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={FiTrash}
|
||||
tooltip={t("delete")}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
/>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={LinkIcon}
|
||||
tooltip={t("copy_link")}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
}}
|
||||
/>
|
||||
<EmbedButton
|
||||
embedUrl={encodeURIComponent(embedLink)}
|
||||
StartIcon={Code}
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
tooltip={t("embed")}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{!isChildrenManagedEventType && (
|
||||
<Button
|
||||
color="destructive"
|
||||
variant="icon"
|
||||
StartIcon={Trash}
|
||||
tooltip={t("delete")}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}
|
||||
/>
|
||||
)}
|
||||
</ButtonGroup>
|
||||
|
||||
<VerticalDivider className="hidden lg:block" />
|
||||
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button className="lg:hidden" StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button className="lg:hidden" StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent style={{ minWidth: "200px" }}>
|
||||
<DropdownMenuItem className="focus:ring-muted">
|
||||
<DropdownItem
|
||||
target="_blank"
|
||||
type="button"
|
||||
StartIcon={FiExternalLink}
|
||||
StartIcon={ExternalLink}
|
||||
href={permalink}
|
||||
rel="noreferrer">
|
||||
{t("preview")}
|
||||
@@ -276,7 +319,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownMenuItem className="focus:ring-muted">
|
||||
<DropdownItem
|
||||
type="button"
|
||||
StartIcon={FiLink}
|
||||
StartIcon={LinkIcon}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(permalink);
|
||||
showToast("Link copied!", "success");
|
||||
@@ -288,7 +331,7 @@ function EventTypeSingleLayout({
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="destructive"
|
||||
StartIcon={FiTrash}
|
||||
StartIcon={Trash}
|
||||
disabled={!hasPermsToDelete}
|
||||
onClick={() => setDeleteDialogOpen(true)}>
|
||||
{t("delete")}
|
||||
@@ -323,7 +366,7 @@ function EventTypeSingleLayout({
|
||||
</Button>
|
||||
</div>
|
||||
}>
|
||||
<Suspense fallback={<FiLoader />}>
|
||||
<Suspense fallback={<Loader />}>
|
||||
<div className="flex flex-col xl:flex-row xl:space-x-6">
|
||||
<div className="hidden xl:block">
|
||||
<VerticalTabs
|
||||
@@ -351,14 +394,25 @@ function EventTypeSingleLayout({
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
variety="danger"
|
||||
title={t("delete_event_type")}
|
||||
confirmBtnText={t("confirm_delete_event_type")}
|
||||
loadingText={t("confirm_delete_event_type")}
|
||||
title={t(`delete${isManagedEvent}_event_type`)}
|
||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||
loadingText={t(`confirm_delete_event_type`)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
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>
|
||||
</Dialog>
|
||||
<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 { useFormContext } from "react-hook-form";
|
||||
|
||||
import useLockedFieldsManager from "@calcom/features/ee/managed-event-types/hooks/useLockedFieldsManager";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Frequency } from "@calcom/prisma/zod-utils";
|
||||
import type { RecurringEvent } from "@calcom/types/Calendar";
|
||||
import { Alert, Select, SettingsToggle } from "@calcom/ui";
|
||||
import { Alert, Select, SettingsToggle, TextField } from "@calcom/ui";
|
||||
|
||||
type RecurringEventControllerProps = {
|
||||
recurringEvent: RecurringEvent | null;
|
||||
eventType: EventTypeSetup;
|
||||
paymentEnabled: boolean;
|
||||
};
|
||||
|
||||
export default function RecurringEventController({
|
||||
recurringEvent,
|
||||
eventType,
|
||||
paymentEnabled,
|
||||
}: RecurringEventControllerProps) {
|
||||
const { t } = useLocale();
|
||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(recurringEvent);
|
||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||
eventType.recurringEvent
|
||||
);
|
||||
const formMethods = useFormContext<FormValues>();
|
||||
|
||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||
@@ -28,6 +31,14 @@ export default function RecurringEventController({
|
||||
value: value.toString(),
|
||||
}));
|
||||
|
||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||
eventType,
|
||||
t("locked_fields_admin_description"),
|
||||
t("locked_fields_member_description")
|
||||
);
|
||||
|
||||
const recurringLocked = shouldLockDisableProps("recurringEvent");
|
||||
|
||||
return (
|
||||
<div className="block items-start sm:flex">
|
||||
<div className={!paymentEnabled ? "w-full" : ""}>
|
||||
@@ -37,6 +48,7 @@ export default function RecurringEventController({
|
||||
<>
|
||||
<SettingsToggle
|
||||
title={t("recurring_event")}
|
||||
{...recurringLocked}
|
||||
description={t("recurring_event_description")}
|
||||
checked={recurringEventState !== null}
|
||||
data-testid="recurring-event-check"
|
||||
@@ -45,7 +57,7 @@ export default function RecurringEventController({
|
||||
formMethods.setValue("recurringEvent", null);
|
||||
setRecurringEventState(null);
|
||||
} else {
|
||||
const newVal = recurringEvent || {
|
||||
const newVal = eventType.recurringEvent || {
|
||||
interval: 1,
|
||||
count: 12,
|
||||
freq: Frequency.WEEKLY,
|
||||
@@ -58,11 +70,12 @@ export default function RecurringEventController({
|
||||
<div data-testid="recurring-event-collapsible" className="text-sm">
|
||||
<div className="flex items-center">
|
||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("repeats_every")}</p>
|
||||
<input
|
||||
<TextField
|
||||
disabled={recurringLocked.disabled}
|
||||
type="number"
|
||||
min="1"
|
||||
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}
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
@@ -77,7 +90,8 @@ export default function RecurringEventController({
|
||||
options={recurringEventFreqOptions}
|
||||
value={recurringEventFreqOptions[recurringEventState.freq]}
|
||||
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) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
@@ -90,12 +104,13 @@ export default function RecurringEventController({
|
||||
</div>
|
||||
<div className="mt-4 flex items-center">
|
||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">{t("for_a_maximum_of")}</p>
|
||||
<input
|
||||
<TextField
|
||||
disabled={recurringLocked.disabled}
|
||||
type="number"
|
||||
min="1"
|
||||
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}
|
||||
className="mb-0"
|
||||
onChange={(event) => {
|
||||
const newVal = {
|
||||
...recurringEventState,
|
||||
@@ -105,7 +120,7 @@ export default function RecurringEventController({
|
||||
setRecurringEventState(newVal);
|
||||
}}
|
||||
/>
|
||||
<p className="text-emphasis ltr:mr-2 rtl:ml-2">
|
||||
<p className="text-emphasis ltr:ml-2 rtl:mr-2">
|
||||
{t("events", {
|
||||
count: recurringEventState.count,
|
||||
})}
|
||||
|
||||
@@ -1,25 +1,29 @@
|
||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||
import type { UnitTypeLongPlural } from "dayjs";
|
||||
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 { useEffect, useState } from "react";
|
||||
import { Controller, useFormContext } from "react-hook-form";
|
||||
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 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 = {
|
||||
metadata: z.infer<typeof EventTypeMetaDataSchema>;
|
||||
requiresConfirmation: boolean;
|
||||
onRequiresConfirmation: Dispatch<SetStateAction<boolean>>;
|
||||
seatsEnabled: boolean;
|
||||
eventType: EventTypeSetup;
|
||||
};
|
||||
|
||||
export default function RequiresConfirmationController({
|
||||
metadata,
|
||||
eventType,
|
||||
requiresConfirmation,
|
||||
onRequiresConfirmation,
|
||||
seatsEnabled,
|
||||
@@ -37,6 +41,23 @@ export default function RequiresConfirmationController({
|
||||
}
|
||||
}, [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 (
|
||||
<div className="block items-start sm:flex">
|
||||
<div className="w-full">
|
||||
@@ -46,10 +67,11 @@ export default function RequiresConfirmationController({
|
||||
render={() => (
|
||||
<SettingsToggle
|
||||
title={t("requires_confirmation")}
|
||||
disabled={seatsEnabled}
|
||||
disabled={seatsEnabled || requiresConfirmationLockedProps.disabled}
|
||||
tooltip={seatsEnabled ? t("seat_options_doesnt_support_confirmation") : undefined}
|
||||
description={t("requires_confirmation_description")}
|
||||
checked={requiresConfirmation}
|
||||
LockedIcon={requiresConfirmationLockedProps.LockedIcon}
|
||||
onCheckedChange={(val) => {
|
||||
formMethods.setValue("requiresConfirmation", 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">
|
||||
<RadioField label={t("always_requires_confirmation")} id="always" value="always" />
|
||||
<RadioField
|
||||
label={
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="when_booked_with_less_than_notice"
|
||||
defaults="When booked with less than <time></time> notice"
|
||||
components={{
|
||||
time: (
|
||||
<div className="mx-2 flex">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
onChange={(evt) => {
|
||||
const val = Number(evt.target?.value);
|
||||
setRequiresConfirmationSetup({
|
||||
unit:
|
||||
requiresConfirmationSetup?.unit ??
|
||||
defaultRequiresConfirmationSetup.unit,
|
||||
time: val,
|
||||
});
|
||||
formMethods.setValue("metadata.requiresConfirmationThreshold.time", val);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
||||
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||
/>
|
||||
<select
|
||||
onChange={(evt) => {
|
||||
const val = evt.target.value as UnitTypeLongPlural;
|
||||
setRequiresConfirmationSetup({
|
||||
time:
|
||||
requiresConfirmationSetup?.time ??
|
||||
defaultRequiresConfirmationSetup.time,
|
||||
unit: 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"
|
||||
defaultValue={
|
||||
metadata?.requiresConfirmationThreshold?.unit ||
|
||||
defaultRequiresConfirmationSetup.unit
|
||||
}>
|
||||
<option value="minutes">{t("minute_timeUnit")}</option>
|
||||
<option value="hours">{t("hour_timeUnit")}</option>
|
||||
</select>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
id="notice"
|
||||
value="notice"
|
||||
/>
|
||||
<div className="flex items-center">
|
||||
<RadioGroup.Item
|
||||
<div className="flex flex-col flex-wrap justify-start gap-y-2">
|
||||
{(requiresConfirmationSetup === undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||
<RadioField
|
||||
label={t("always_requires_confirmation")}
|
||||
disabled={requiresConfirmationLockedProps.disabled}
|
||||
id="always"
|
||||
value="always"
|
||||
/>
|
||||
)}
|
||||
{(requiresConfirmationSetup !== undefined || !requiresConfirmationLockedProps.disabled) && (
|
||||
<RadioField
|
||||
disabled={requiresConfirmationLockedProps.disabled}
|
||||
className="items-center"
|
||||
label={
|
||||
<>
|
||||
<Trans
|
||||
i18nKey="when_booked_with_less_than_notice"
|
||||
defaults="When booked with less than <time></time> notice"
|
||||
components={{
|
||||
time: (
|
||||
<div className="mx-2 inline-flex">
|
||||
<Input
|
||||
type="number"
|
||||
min={1}
|
||||
disabled={requiresConfirmationLockedProps.disabled}
|
||||
onChange={(evt) => {
|
||||
const val = Number(evt.target?.value);
|
||||
setRequiresConfirmationSetup({
|
||||
unit:
|
||||
requiresConfirmationSetup?.unit ??
|
||||
defaultRequiresConfirmationSetup.unit,
|
||||
time: val,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.time",
|
||||
val
|
||||
);
|
||||
}}
|
||||
className="border-default !m-0 block w-16 rounded-md text-sm [appearance:textfield]"
|
||||
defaultValue={metadata?.requiresConfirmationThreshold?.time || 30}
|
||||
/>
|
||||
<label
|
||||
className={classNames(
|
||||
requiresConfirmationLockedProps.disabled && "cursor-not-allowed"
|
||||
)}>
|
||||
<Select
|
||||
inputId="notice"
|
||||
options={options}
|
||||
isSearchable={false}
|
||||
isDisabled={requiresConfirmationLockedProps.disabled}
|
||||
className="ml-2"
|
||||
onChange={(opt) => {
|
||||
setRequiresConfirmationSetup({
|
||||
time:
|
||||
requiresConfirmationSetup?.time ??
|
||||
defaultRequiresConfirmationSetup.time,
|
||||
unit: opt?.value as UnitTypeLongPlural,
|
||||
});
|
||||
formMethods.setValue(
|
||||
"metadata.requiresConfirmationThreshold.unit",
|
||||
opt?.value as UnitTypeLongPlural
|
||||
);
|
||||
}}
|
||||
defaultValue={defaultValue}
|
||||
/>
|
||||
</label>
|
||||
</div>
|
||||
),
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
id="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>
|
||||
</RadioGroup.Root>
|
||||
</SettingsToggle>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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() {
|
||||
return (
|
||||
@@ -32,11 +32,11 @@ function SkeletonItem() {
|
||||
<div className="">
|
||||
<ul className="mt-2 flex space-x-4 rtl:space-x-reverse ">
|
||||
<li className="flex items-center whitespace-nowrap">
|
||||
<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" />
|
||||
</li>
|
||||
<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" />
|
||||
</li>
|
||||
</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 }) => {
|
||||
return (
|
||||
<div className="mb-4 sm:flex sm:items-start">
|
||||
<div className="bg-brand text-brandcontrast dark:bg-darkmodebrand dark:text-darkmodebrandcontrast mx-auto flex h-12 w-12 flex-shrink-0 items-center justify-center rounded-full bg-opacity-5 sm:mx-0 sm:h-10 sm:w-10">
|
||||
<FiShield className="text-inverted h-6 w-6" />
|
||||
<Shield className="text-inverted h-6 w-6" />
|
||||
</div>
|
||||
<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">
|
||||
|
||||
@@ -3,7 +3,7 @@ import { useMutation } from "@tanstack/react-query";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
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: {
|
||||
type: string;
|
||||
@@ -76,7 +76,7 @@ export function CalendarSwitch(props: {
|
||||
{props.defaultSelected && (
|
||||
<Badge variant="gray">
|
||||
<div className="flex">
|
||||
<FiArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
|
||||
<ArrowLeft className="text-default mr-1" /> {t("adding_events_to")}
|
||||
</div>
|
||||
</Badge>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { isPasswordValid } from "@calcom/features/auth/lib/isPasswordValid";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 }) => {
|
||||
const { t } = useLocale();
|
||||
@@ -24,7 +24,7 @@ export const AdminUserContainer = (props: React.ComponentProps<typeof AdminUser>
|
||||
props.onSuccess();
|
||||
}}>
|
||||
<EmptyScreen
|
||||
Icon={FiUserCheck}
|
||||
Icon={UserCheck}
|
||||
headline={t("admin_user_created")}
|
||||
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 { trpc } from "@calcom/trpc/react";
|
||||
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 = {
|
||||
licenseKey: string;
|
||||
@@ -91,7 +91,7 @@ const EnterpriseLicense = (
|
||||
<div>
|
||||
<Button
|
||||
className="w-full justify-center text-lg"
|
||||
EndIcon={FiExternalLink}
|
||||
EndIcon={ExternalLink}
|
||||
href="https://console.cal.com"
|
||||
target="_blank">
|
||||
{t("purchase_license")}
|
||||
@@ -121,9 +121,9 @@ const EnterpriseLicense = (
|
||||
)}
|
||||
addOnSuffix={
|
||||
checkLicenseLoading ? (
|
||||
<FiLoader className="h-5 w-5 animate-spin" />
|
||||
<Loader className="h-5 w-5 animate-spin" />
|
||||
) : errors.licenseKey === undefined && isDirty ? (
|
||||
<FiCheck className="h-5 w-5 text-green-700" />
|
||||
<Check className="h-5 w-5 text-green-700" />
|
||||
) : undefined
|
||||
}
|
||||
color={errors.licenseKey ? "warn" : ""}
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useRouter } from "next/router";
|
||||
import type { Dispatch, SetStateAction } from "react";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { FiCheck } from "@calcom/ui/components/icon";
|
||||
import { Check } from "@calcom/ui/components/icon";
|
||||
|
||||
const StepDone = (props: {
|
||||
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="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 className="max-w-[420px] text-center">
|
||||
<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 type { ControllerRenderProps } from "react-hook-form";
|
||||
|
||||
import { FiEdit2 } from "@calcom/ui/components/icon";
|
||||
import { Edit2 } from "@calcom/ui/components/icon";
|
||||
|
||||
const EditableHeading = function EditableHeading({
|
||||
value,
|
||||
@@ -21,7 +21,7 @@ const EditableHeading = function EditableHeading({
|
||||
<label className="min-w-8 relative inline-block">
|
||||
<span className="whitespace-pre text-xl tracking-normal text-transparent">{value} </span>
|
||||
{!isEditing && isReady && (
|
||||
<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
|
||||
{...passThroughProps}
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
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 }) {
|
||||
return (
|
||||
<>
|
||||
<Tooltip side="top" content={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>
|
||||
</Tooltip>
|
||||
</>
|
||||
|
||||
@@ -15,7 +15,7 @@ import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
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 {
|
||||
UPGRADE = "UPGRADE",
|
||||
@@ -235,7 +235,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
usernameIsAvailable ? "" : ""
|
||||
)}>
|
||||
{isInputUsernamePremium ? <StarIconSolid className="mt-[2px] w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <FiCheck className="mt-2 w-6" /> : <></>}
|
||||
{!isInputUsernamePremium && usernameIsAvailable ? <Check className="mt-2 w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@@ -251,7 +251,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
|
||||
<Dialog open={openDialogSaveUsername}>
|
||||
<DialogContent
|
||||
Icon={FiEdit2}
|
||||
Icon={Edit2}
|
||||
title={t("confirm_username_change_dialog_title")}
|
||||
description={
|
||||
<>
|
||||
@@ -288,7 +288,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
||||
data-testid="go-to-billing"
|
||||
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>
|
||||
)}
|
||||
|
||||
@@ -9,7 +9,7 @@ import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
||||
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 {
|
||||
currentUsername: string | undefined;
|
||||
@@ -137,7 +137,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
{currentUsername !== inputUsernameValue && (
|
||||
<div className="absolute right-[2px] top-6 flex flex-row">
|
||||
<span className={classNames("mx-2 py-2")}>
|
||||
{usernameIsAvailable ? <FiCheck className="w-6" /> : <></>}
|
||||
{usernameIsAvailable ? <Check className="w-6" /> : <></>}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
@@ -154,7 +154,7 @@ const UsernameTextfield = (props: ICustomUsernameProps) => {
|
||||
</div>
|
||||
)}
|
||||
<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="mb-4 w-full pt-1">
|
||||
<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 { 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";
|
||||
|
||||
@@ -49,7 +49,7 @@ export const CheckedSelect = ({
|
||||
alt={option.label}
|
||||
/>
|
||||
{option.label}
|
||||
<FiX
|
||||
<X
|
||||
onClick={() => props.onChange(value.filter((item) => item.value !== option.value))}
|
||||
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 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";
|
||||
|
||||
@@ -23,7 +23,7 @@ export const DatePicker = ({ minDate, disabled, date, onDatesChange, className }
|
||||
className
|
||||
)}
|
||||
clearIcon={null}
|
||||
calendarIcon={<FiCalendar className="text-subtle h-5 w-5" />}
|
||||
calendarIcon={<Calendar className="text-subtle h-5 w-5" />}
|
||||
value={date}
|
||||
minDate={minDate}
|
||||
disabled={disabled}
|
||||
|
||||
@@ -84,14 +84,14 @@ const CalcomThemeProvider = (
|
||||
// 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
|
||||
const embedNamespace = typeof window !== "undefined" ? window.getEmbedNamespace() : null;
|
||||
const isEmbedMode = typeof embedNamespace === "string";
|
||||
// If embedNamespace is not defined, we use the default storageKey -> The default storage key changs based on if we force light mode or not
|
||||
// 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 =
|
||||
typeof embedNamespace === "string"
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: !isThemeSupported
|
||||
? "cal-light"
|
||||
: "theme";
|
||||
const storageKey = isEmbedMode
|
||||
? `embed-theme-${embedNamespace}`
|
||||
: !isThemeSupported
|
||||
? "cal-light"
|
||||
: "theme";
|
||||
|
||||
return (
|
||||
<ThemeProvider
|
||||
@@ -101,6 +101,17 @@ const CalcomThemeProvider = (
|
||||
forcedTheme={forcedTheme}
|
||||
storageKey={storageKey}
|
||||
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}
|
||||
</ThemeProvider>
|
||||
);
|
||||
@@ -121,7 +132,6 @@ const AppProviders = (props: AppPropsWithChildren) => {
|
||||
<SessionProvider session={session || undefined}>
|
||||
<CustomI18nextProvider {...props}>
|
||||
<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
|
||||
nonce={props.pageProps.nonce}
|
||||
isThemeSupported={props.Component.isThemeSupported}>
|
||||
|
||||
@@ -92,11 +92,11 @@ const nextConfig = {
|
||||
"@calcom/prisma",
|
||||
"@calcom/trpc",
|
||||
"@calcom/ui",
|
||||
"lucide-react",
|
||||
],
|
||||
modularizeImports: {
|
||||
"@calcom/ui/components/icon": {
|
||||
transform: "@react-icons/all-files/fi/{{member}}",
|
||||
skipDefaultConversion: true,
|
||||
transform: "lucide-react/dist/esm/icons/{{ kebabCase member }}",
|
||||
preventFullImport: true,
|
||||
},
|
||||
"@calcom/features/insights/components": {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.7.12",
|
||||
"version": "2.8.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
||||
+12
-12
@@ -6,7 +6,7 @@ import { useEffect, useState } from "react";
|
||||
import { DOCS_URL, JOIN_SLACK, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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 { ssgInit } from "@server/lib/ssg";
|
||||
|
||||
@@ -20,13 +20,13 @@ export default function Custom404() {
|
||||
{
|
||||
title: t("documentation"),
|
||||
description: t("documentation_description"),
|
||||
icon: FiFileText,
|
||||
icon: FileText,
|
||||
href: DOCS_URL,
|
||||
},
|
||||
{
|
||||
title: t("blog"),
|
||||
description: t("blog_description"),
|
||||
icon: FiBookOpen,
|
||||
icon: BookOpen,
|
||||
href: `${WEBSITE_URL}/blog`,
|
||||
},
|
||||
];
|
||||
@@ -112,7 +112,7 @@ export default function Custom404() {
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||
<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>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -127,7 +127,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{t("the_infrastructure_plan")}</p>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
@@ -140,7 +140,7 @@ export default function Custom404() {
|
||||
className="relative flex items-start space-x-4 py-6 rtl:space-x-reverse">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="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>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -153,7 +153,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{t("prisma_studio_tip_description")}</p>
|
||||
</div>
|
||||
<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>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -198,7 +198,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
@@ -257,7 +257,7 @@ export default function Custom404() {
|
||||
rel="noreferrer">
|
||||
<div className="flex-shrink-0">
|
||||
<span className="flex h-12 w-12 items-center justify-center rounded-lg bg-green-50">
|
||||
<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>
|
||||
</div>
|
||||
<div className="min-w-0 flex-1">
|
||||
@@ -272,7 +272,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{t("claim_username_and_schedule_events")}</p>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
@@ -300,7 +300,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{link.description}</p>
|
||||
</div>
|
||||
<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>
|
||||
</Link>
|
||||
</li>
|
||||
@@ -346,7 +346,7 @@ export default function Custom404() {
|
||||
<p className="text-subtle text-base">{t("join_our_community")}</p>
|
||||
</div>
|
||||
<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>
|
||||
</a>
|
||||
</li>
|
||||
|
||||
@@ -4,7 +4,7 @@ import { useRouter } from "next/router";
|
||||
import { APP_NAME, WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, showToast } from "@calcom/ui";
|
||||
import { FiCopy } from "@calcom/ui/components/icon";
|
||||
import { Copy } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function Error500() {
|
||||
const { t } = useLocale();
|
||||
@@ -34,7 +34,7 @@ export default function Error500() {
|
||||
<Button
|
||||
color="secondary"
|
||||
className="mt-2 border-0 font-sans font-normal hover:bg-gray-300"
|
||||
StartIcon={FiCopy}
|
||||
StartIcon={Copy}
|
||||
onClick={() => {
|
||||
navigator.clipboard.writeText(router.query.error as string);
|
||||
showToast("Link copied!", "success");
|
||||
|
||||
@@ -29,7 +29,7 @@ import prisma from "@calcom/prisma";
|
||||
import { baseEventTypeSelect } from "@calcom/prisma/selects";
|
||||
import { EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
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 { EmbedProps } from "@lib/withEmbedSsr";
|
||||
@@ -60,7 +60,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<li
|
||||
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">
|
||||
<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
|
||||
href={getUsernameSlugLink({ users: props.users, slug: type.slug })}
|
||||
className="flex justify-between px-6 py-4"
|
||||
@@ -122,7 +122,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<main
|
||||
className={classNames(
|
||||
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"
|
||||
)}>
|
||||
{isSingleUser && ( // When we deal with a single user, not dynamic group
|
||||
@@ -161,7 +161,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
key={type.id}
|
||||
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">
|
||||
<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 */}
|
||||
<div className="block w-full p-5">
|
||||
<Link
|
||||
@@ -180,7 +180,7 @@ export default function User(props: inferSSRProps<typeof getServerSideProps> & E
|
||||
<div className="flex flex-wrap items-center">
|
||||
<h2 className=" text-default pr-2 text-sm font-semibold">{type.title}</h2>
|
||||
</div>
|
||||
<EventTypeDescription eventType={type} />
|
||||
<EventTypeDescription eventType={type} isPublic={true} />
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -5,6 +5,7 @@ import Head from "next/head";
|
||||
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 { trpc } from "@calcom/trpc/react";
|
||||
|
||||
@@ -27,6 +28,18 @@ const calFont = localFont({
|
||||
function MyApp(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") {
|
||||
@@ -62,6 +75,12 @@ function MyApp(props: AppProps) {
|
||||
--font-cal: ${calFont.style.fontFamily};
|
||||
}
|
||||
`}</style>
|
||||
|
||||
<style jsx global>
|
||||
{`
|
||||
${cssVarsStyle.join("")}
|
||||
`}
|
||||
</style>
|
||||
<Head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1" />
|
||||
</Head>
|
||||
|
||||
@@ -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 }),
|
||||
});
|
||||
@@ -5,7 +5,7 @@ import { getAppRegistry } from "@calcom/app-store/_appRegistry";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { SkeletonText } from "@calcom/ui";
|
||||
import { FiArrowLeft, FiArrowRight } from "@calcom/ui/components/icon";
|
||||
import { ArrowLeft, ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function Apps({ categories }: InferGetStaticPropsType<typeof getStaticProps>) {
|
||||
const { t, isLocaleReady } = useLocale();
|
||||
@@ -16,7 +16,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
<Link
|
||||
href="/apps"
|
||||
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" />}{" "}
|
||||
</Link>
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@ export default function Apps({ categories }: InferGetStaticPropsType<typeof getS
|
||||
<h3 className="font-medium capitalize">{category.name}</h3>
|
||||
<p className="text-subtle text-sm">
|
||||
{t("number_apps", { count: category.count })}{" "}
|
||||
<FiArrowRight className="inline-block h-4 w-4" />
|
||||
<ArrowRight className="inline-block h-4 w-4" />
|
||||
</p>
|
||||
</div>
|
||||
</Link>
|
||||
|
||||
@@ -10,7 +10,7 @@ import type { AppCategories } from "@calcom/prisma/client";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import type { HorizontalTabItemProps } 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 AppsLayout from "@components/apps/layouts/AppsLayout";
|
||||
|
||||
@@ -37,7 +37,7 @@ function AppsSearch({
|
||||
return (
|
||||
<TextField
|
||||
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"
|
||||
containerClassName={classNames("focus:!ring-offset-0", className)}
|
||||
type="search"
|
||||
|
||||
@@ -30,15 +30,15 @@ import {
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiBarChart,
|
||||
FiCalendar,
|
||||
FiCreditCard,
|
||||
FiGrid,
|
||||
FiMoreHorizontal,
|
||||
FiPlus,
|
||||
FiShare2,
|
||||
FiTrash,
|
||||
FiVideo,
|
||||
BarChart,
|
||||
Calendar,
|
||||
CreditCard,
|
||||
Grid,
|
||||
MoreHorizontal,
|
||||
Plus,
|
||||
Share2,
|
||||
Trash,
|
||||
Video,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
@@ -71,7 +71,7 @@ function ConnectOrDisconnectIntegrationMenuItem(props: {
|
||||
color="destructive"
|
||||
onClick={() => handleDisconnect(credentialId)}
|
||||
disabled={isGlobal}
|
||||
StartIcon={FiTrash}>
|
||||
StartIcon={Trash}>
|
||||
{t("remove_app")}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
@@ -159,7 +159,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
||||
<div className="flex justify-end">
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!appIsDefault && variant === "conferencing" && (
|
||||
@@ -167,7 +167,7 @@ const IntegrationsList = ({ data, handleDisconnect, variant }: IntegrationsListP
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon={FiVideo}
|
||||
StartIcon={Video}
|
||||
onClick={() => {
|
||||
const locationType = getEventLocationTypeFromApp(
|
||||
item?.locationOption?.value ?? ""
|
||||
@@ -225,13 +225,13 @@ const IntegrationsContainer = ({
|
||||
const { t } = useLocale();
|
||||
const query = trpc.viewer.integrations.useQuery({ variant, exclude, onlyInstalled: true });
|
||||
const emptyIcon = {
|
||||
calendar: FiCalendar,
|
||||
conferencing: FiVideo,
|
||||
automation: FiShare2,
|
||||
analytics: FiBarChart,
|
||||
payment: FiCreditCard,
|
||||
web3: FiBarChart,
|
||||
other: FiGrid,
|
||||
calendar: Calendar,
|
||||
conferencing: Video,
|
||||
automation: Share2,
|
||||
analytics: BarChart,
|
||||
payment: CreditCard,
|
||||
web3: BarChart,
|
||||
other: Grid,
|
||||
};
|
||||
|
||||
return (
|
||||
@@ -270,7 +270,7 @@ const IntegrationsContainer = ({
|
||||
variant ? `/apps/categories/${variant === "conferencing" ? "video" : variant}` : "/apps"
|
||||
}
|
||||
color="secondary"
|
||||
StartIcon={FiPlus}>
|
||||
StartIcon={Plus}>
|
||||
{t("add")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -5,7 +5,7 @@ import z from "zod";
|
||||
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, SkeletonText } from "@calcom/ui";
|
||||
import { FiX } from "@calcom/ui/components/icon";
|
||||
import { X } from "@calcom/ui/components/icon";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function Error() {
|
||||
<AuthContainer title="" description="">
|
||||
<div>
|
||||
<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 className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -4,6 +4,7 @@ import type { GetServerSidePropsContext } from "next";
|
||||
import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import type { CSSProperties } from "react";
|
||||
import React, { useMemo } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
@@ -109,6 +110,14 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
||||
<>
|
||||
<form
|
||||
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) => {
|
||||
e.preventDefault();
|
||||
|
||||
@@ -164,6 +173,8 @@ export default function Page({ resetPasswordRequest, csrfToken }: Props) {
|
||||
);
|
||||
}
|
||||
|
||||
Page.isThemeSupported = false;
|
||||
|
||||
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
const id = context.params?.id as string;
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ import { getCsrfToken } from "next-auth/react";
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import type { SyntheticEvent } from "react";
|
||||
import type { CSSProperties, SyntheticEvent } from "react";
|
||||
import React from "react";
|
||||
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
@@ -105,7 +105,18 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
{!success && (
|
||||
<>
|
||||
<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 />
|
||||
<EmailField
|
||||
onChange={handleChange}
|
||||
@@ -132,6 +143,8 @@ export default function ForgotPassword({ csrfToken }: { csrfToken: string }) {
|
||||
);
|
||||
}
|
||||
|
||||
ForgotPassword.isThemeSupported = false;
|
||||
|
||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
||||
const { req, res } = context;
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
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 { WithNonceProps } from "@lib/withNonce";
|
||||
@@ -91,7 +91,7 @@ export default function Login({
|
||||
setTwoFactorRequired(false);
|
||||
methods.setValue("totpCode", "");
|
||||
}}
|
||||
StartIcon={FiArrowLeft}
|
||||
StartIcon={ArrowLeft}
|
||||
color="minimal">
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useEffect } from "react";
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
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";
|
||||
|
||||
@@ -32,7 +32,7 @@ export function Logout(props: Props) {
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||
<div className="mb-4">
|
||||
<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 className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-title">
|
||||
|
||||
@@ -27,7 +27,7 @@ import {
|
||||
ConfirmationDialogContent,
|
||||
VerticalDivider,
|
||||
} 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";
|
||||
|
||||
@@ -57,7 +57,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
{t("date_overrides")}{" "}
|
||||
<Tooltip content={t("date_overrides_info")}>
|
||||
<span className="inline-block">
|
||||
<FiInfo />
|
||||
<Info className="h-4 w-4" />
|
||||
</span>
|
||||
</Tooltip>
|
||||
</h3>
|
||||
@@ -75,7 +75,7 @@ const DateOverride = ({ workingHours }: { workingHours: WorkingHours[] }) => {
|
||||
excludedDates={fields.map((field) => yyyymmdd(field.ranges[0].start))}
|
||||
onChange={(ranges) => append({ ranges })}
|
||||
Trigger={
|
||||
<Button color="secondary" StartIcon={FiPlus} data-testid="add-override">
|
||||
<Button color="secondary" StartIcon={Plus} data-testid="add-override">
|
||||
Add an override
|
||||
</Button>
|
||||
}
|
||||
@@ -202,7 +202,7 @@ export default function Availability() {
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button
|
||||
StartIcon={FiTrash}
|
||||
StartIcon={Trash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
|
||||
@@ -6,7 +6,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
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 { HttpError } from "@lib/core/http/error";
|
||||
@@ -75,7 +75,7 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
{schedules.length === 0 ? (
|
||||
<div className="flex justify-center">
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
Icon={Clock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
buttonRaw={<NewScheduleButton />}
|
||||
|
||||
@@ -49,7 +49,7 @@ import type { Prisma } from "@calcom/prisma/client";
|
||||
import { bookingMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||
import { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
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 type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -177,7 +177,7 @@ export default function Success(props: SuccessProps) {
|
||||
// - Event Type has require confirmation option enabled always
|
||||
// - EventType has conditionally enabled confirmation option based on how far the booking is scheduled.
|
||||
// - 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 isCancelled =
|
||||
@@ -306,7 +306,7 @@ export default function Success(props: SuccessProps) {
|
||||
<Link
|
||||
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">
|
||||
<FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
<ChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
</Link>
|
||||
</div>
|
||||
)}
|
||||
@@ -325,7 +325,7 @@ export default function Success(props: SuccessProps) {
|
||||
<div
|
||||
className={classNames(
|
||||
"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"
|
||||
)}
|
||||
role="dialog"
|
||||
@@ -347,10 +347,10 @@ export default function Success(props: SuccessProps) {
|
||||
<img src={giphyImage} alt="Gif from Giphy" />
|
||||
)}
|
||||
{!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" />}
|
||||
{isCancelled && <FiX className="h-5 w-5 text-red-600" />}
|
||||
{needsConfirmation && !isCancelled && <Calendar className="text-emphasis h-5 w-5" />}
|
||||
{isCancelled && <X className="h-5 w-5 text-red-600" />}
|
||||
</div>
|
||||
<div className="mt-6 mb-8 text-center last:mb-0">
|
||||
<h3
|
||||
@@ -458,7 +458,7 @@ export default function Success(props: SuccessProps) {
|
||||
className="text-default flex items-center gap-2 underline"
|
||||
rel="noreferrer">
|
||||
{providerName || "Link"}
|
||||
<FiExternalLink className="text-default inline h-4 w-4" />
|
||||
<ExternalLink className="text-default inline h-4 w-4" />
|
||||
</a>
|
||||
) : (
|
||||
locationToDisplay
|
||||
|
||||
@@ -12,7 +12,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
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";
|
||||
|
||||
@@ -180,7 +180,7 @@ export default function Bookings() {
|
||||
{query.status === "success" && isEmpty && (
|
||||
<div className="flex items-center justify-center pt-2 xl:pt-0">
|
||||
<EmptyScreen
|
||||
Icon={FiCalendar}
|
||||
Icon={Calendar}
|
||||
headline={t("no_status_bookings_yet", { status: t(status).toLowerCase() })}
|
||||
description={t("no_status_bookings_yet_description", {
|
||||
status: t(status).toLowerCase(),
|
||||
|
||||
@@ -4,6 +4,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import type { PeriodType } from "@prisma/client";
|
||||
import type { SchedulingType } from "@prisma/client";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import { Trans } from "next-i18next";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { z } from "zod";
|
||||
@@ -11,26 +12,28 @@ import { z } from "zod";
|
||||
import { validateCustomEventName } from "@calcom/core/event";
|
||||
import type { EventLocationType } from "@calcom/core/location";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
import type { ChildrenEventType } from "@calcom/features/eventtypes/components/ChildrenEventTypeSelect";
|
||||
import { validateIntervalLimitOrder } from "@calcom/lib";
|
||||
import { CAL_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { useTypedQuery } from "@calcom/lib/hooks/useTypedQuery";
|
||||
import { HttpError } from "@calcom/lib/http-error";
|
||||
import { useTelemetry, telemetryEventTypes } from "@calcom/lib/telemetry";
|
||||
import type { Prisma } from "@calcom/prisma/client";
|
||||
import { eventTypeBookingFields } from "@calcom/prisma/zod-utils";
|
||||
import type { customInputSchema, EventTypeMetaDataSchema } from "@calcom/prisma/zod-utils";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
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 type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import { AvailabilityTab } from "@components/eventtype/AvailabilityTab";
|
||||
// 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 { EventAppsTab } from "@components/eventtype/EventAppsTab";
|
||||
import { EventAvailabilityTab } from "@components/eventtype/EventAvailabilityTab";
|
||||
import { EventLimitsTab } from "@components/eventtype/EventLimitsTab";
|
||||
import { EventRecurringTab } from "@components/eventtype/EventRecurringTab";
|
||||
import { EventSetupTab } from "@components/eventtype/EventSetupTab";
|
||||
@@ -87,6 +90,7 @@ export type FormValues = {
|
||||
successRedirectUrl: string;
|
||||
durationLimits?: IntervalLimit;
|
||||
bookingLimits?: IntervalLimit;
|
||||
children: ChildrenEventType[];
|
||||
hosts: { userId: number; isFixed: boolean }[];
|
||||
bookingFields: z.infer<typeof eventTypeBookingFields>;
|
||||
};
|
||||
@@ -111,10 +115,12 @@ const querySchema = z.object({
|
||||
});
|
||||
|
||||
export type EventTypeSetupProps = RouterOutputs["viewer"]["eventTypes"]["get"];
|
||||
export type EventTypeSetup = RouterOutputs["viewer"]["eventTypes"]["get"]["eventType"];
|
||||
|
||||
const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const { t } = useLocale();
|
||||
const utils = trpc.useContext();
|
||||
const telemetry = useTelemetry();
|
||||
const {
|
||||
data: { tabName },
|
||||
} = useTypedQuery(querySchema);
|
||||
@@ -208,6 +214,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||
metadata,
|
||||
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;
|
||||
|
||||
const formMethods = useForm<FormValues>({
|
||||
@@ -266,8 +284,8 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
destinationCalendar={destinationCalendar}
|
||||
/>
|
||||
),
|
||||
availability: <AvailabilityTab isTeamEvent={!!team} />,
|
||||
team: <EventTeamTab teamMembers={teamMembers} team={team} />,
|
||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||
limits: <EventLimitsTab eventType={eventType} />,
|
||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||
recurring: <EventRecurringTab eventType={eventType} />,
|
||||
@@ -281,85 +299,143 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
webhooks: <EventTeamWebhooksTab eventType={eventType} team={team} />,
|
||||
} 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 (
|
||||
<EventTypeSingleLayout
|
||||
enabledAppsNumber={numberOfActiveApps}
|
||||
installedAppsNumber={numberOfInstalledApps}
|
||||
enabledWorkflowsNumber={eventType.workflows.length}
|
||||
eventType={eventType}
|
||||
team={team}
|
||||
isUpdateMutationLoading={updateMutation.isLoading}
|
||||
formMethods={formMethods}
|
||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||
currentUserMembership={currentUserMembership}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values) => {
|
||||
const {
|
||||
periodDates,
|
||||
periodCountCalendarDays,
|
||||
beforeBufferTime,
|
||||
afterBufferTime,
|
||||
seatsPerTimeSlot,
|
||||
seatsShowAttendees,
|
||||
bookingLimits,
|
||||
durationLimits,
|
||||
recurringEvent,
|
||||
locations,
|
||||
metadata,
|
||||
customInputs,
|
||||
// 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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
<>
|
||||
<EventTypeSingleLayout
|
||||
enabledAppsNumber={numberOfActiveApps}
|
||||
installedAppsNumber={numberOfInstalledApps}
|
||||
enabledWorkflowsNumber={eventType.workflows.length}
|
||||
eventType={eventType}
|
||||
team={team}
|
||||
isUpdateMutationLoading={updateMutation.isLoading}
|
||||
formMethods={formMethods}
|
||||
disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||
currentUserMembership={currentUserMembership}>
|
||||
<Form
|
||||
form={formMethods}
|
||||
id="event-type-form"
|
||||
handleSubmit={async (values: FormValues) => {
|
||||
if (!values.children.length) return handleSubmit(values);
|
||||
const existingSlugEventTypes = values.children.filter((ch) =>
|
||||
ch.owner.eventTypeSlugs.includes(slug)
|
||||
);
|
||||
if (!existingSlugEventTypes.length) return handleSubmit(values);
|
||||
setSlugExistsChildrenDialogOpen(existingSlugEventTypes);
|
||||
}}>
|
||||
<div ref={animationParentRef}>{tabMap[tabName]}</div>
|
||||
</Form>
|
||||
</EventTypeSingleLayout>
|
||||
<Dialog
|
||||
open={slugExistsChildrenDialogOpen.length > 0}
|
||||
onOpenChange={() => {
|
||||
setSlugExistsChildrenDialogOpen([]);
|
||||
}}>
|
||||
<div ref={animationParentRef} className="space-y-6">
|
||||
{tabMap[tabName]}
|
||||
</div>
|
||||
</Form>
|
||||
</EventTypeSingleLayout>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={formMethods.formState.isSubmitting}
|
||||
variety="warning"
|
||||
title={t("managed_event_dialog_title", {
|
||||
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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
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 { useRouter } from "next/router";
|
||||
import type { FC } from "react";
|
||||
@@ -39,19 +42,19 @@ import {
|
||||
HeadSeo,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiArrowDown,
|
||||
FiArrowUp,
|
||||
FiClipboard,
|
||||
FiCode,
|
||||
FiCopy,
|
||||
FiEdit,
|
||||
FiEdit2,
|
||||
FiExternalLink,
|
||||
FiLink,
|
||||
FiMoreHorizontal,
|
||||
FiTrash,
|
||||
FiUpload,
|
||||
FiUsers,
|
||||
ArrowDown,
|
||||
ArrowUp,
|
||||
Clipboard,
|
||||
Code,
|
||||
Copy,
|
||||
Edit,
|
||||
Edit2,
|
||||
ExternalLink,
|
||||
Link as LinkIcon,
|
||||
MoreHorizontal,
|
||||
Trash,
|
||||
Upload,
|
||||
Users,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import { withQuery } from "@lib/QueryCell";
|
||||
@@ -116,7 +119,40 @@ const MobileTeamsTab: FC<MobileTeamsTabProps> = (props) => {
|
||||
const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGroup; readOnly: boolean }) => {
|
||||
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
|
||||
href={`/event-types/${type.id}?tabName=setup`}
|
||||
className="flex-1 overflow-hidden pr-4 text-sm"
|
||||
@@ -157,6 +193,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
const [parent] = useAutoAnimate<HTMLUListElement>();
|
||||
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
|
||||
const [deleteDialogTypeId, setDeleteDialogTypeId] = useState(0);
|
||||
const [deleteDialogTypeSchedulingType, setDeleteDialogSchedulingType] = useState<SchedulingType | null>(
|
||||
null
|
||||
);
|
||||
const utils = trpc.useContext();
|
||||
const mutation = trpc.viewer.eventTypeOrder.useMutation({
|
||||
onError: async (err) => {
|
||||
@@ -317,6 +356,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
{types.map((type, index) => {
|
||||
const embedLink = `${group.profile.slug}/${type.slug}`;
|
||||
const calLink = `${CAL_URL}/${embedLink}`;
|
||||
const isManagedEventType = type.schedulingType === SchedulingType.MANAGED;
|
||||
const isChildrenManagedEventType =
|
||||
type.metadata?.managedEventConfig !== undefined && type.schedulingType !== SchedulingType.MANAGED;
|
||||
return (
|
||||
<li key={type.id}>
|
||||
<div className="hover:bg-muted flex w-full items-center justify-between">
|
||||
@@ -325,7 +367,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<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]"
|
||||
onClick={() => moveEventType(index, -1)}>
|
||||
<FiArrowUp className="h-5 w-5" />
|
||||
<ArrowUp className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
|
||||
@@ -333,116 +375,148 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<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]"
|
||||
onClick={() => moveEventType(index, 1)}>
|
||||
<FiArrowDown className="h-5 w-5" />
|
||||
<ArrowDown className="h-5 w-5" />
|
||||
</button>
|
||||
)}
|
||||
<MemoizedItem type={type} group={group} readOnly={readOnly} />
|
||||
<div className="mt-4 hidden sm:mt-0 sm:flex">
|
||||
<div className="flex justify-between space-x-2 rtl:space-x-reverse">
|
||||
{type.team && (
|
||||
{type.team && !isManagedEventType && (
|
||||
<AvatarGroup
|
||||
className="relative top-1 right-3"
|
||||
size="sm"
|
||||
truncateAfter={4}
|
||||
items={type.users.map((organizer) => ({
|
||||
items={type.users.map((organizer: { name: any; username: any }) => ({
|
||||
alt: organizer.name || "",
|
||||
image: `${WEBAPP_URL}/${organizer.username}/avatar.png`,
|
||||
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">
|
||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
{type.hidden && <Badge variant="gray">{t("hidden")}</Badge>}
|
||||
<Tooltip content={t("show_eventtype_on_profile")}>
|
||||
<div className="self-center rounded-md p-2">
|
||||
<Switch
|
||||
name="Hidden"
|
||||
checked={!type.hidden}
|
||||
onCheckedChange={() => {
|
||||
setHiddenMutation.mutate({ id: type.id, hidden: !type.hidden });
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
|
||||
<ButtonGroup combined>
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
data-testid="preview-link-button"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={calLink}
|
||||
StartIcon={FiExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
{!isManagedEventType && (
|
||||
<>
|
||||
<Tooltip content={t("preview")}>
|
||||
<Button
|
||||
data-testid="preview-link-button"
|
||||
color="secondary"
|
||||
target="_blank"
|
||||
variant="icon"
|
||||
href={calLink}
|
||||
StartIcon={ExternalLink}
|
||||
/>
|
||||
</Tooltip>
|
||||
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={FiLink}
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
<Tooltip content={t("copy_link")}>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon={LinkIcon}
|
||||
onClick={() => {
|
||||
showToast(t("link_copied"), "success");
|
||||
navigator.clipboard.writeText(calLink);
|
||||
}}
|
||||
/>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
<Dropdown modal={false}>
|
||||
<DropdownMenuTrigger asChild data-testid={"event-type-options-" + type.id}>
|
||||
<Button
|
||||
type="button"
|
||||
variant="icon"
|
||||
color="secondary"
|
||||
StartIcon={FiMoreHorizontal}
|
||||
StartIcon={MoreHorizontal}
|
||||
className="ltr:radix-state-open:rounded-r-md rtl:radix-state-open:rounded-l-md"
|
||||
/>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
<DropdownMenuItem>
|
||||
<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) && (
|
||||
{!readOnly && (
|
||||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
color="destructive"
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
}}
|
||||
StartIcon={FiTrash}
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
type="button"
|
||||
data-testid={"event-type-edit-" + type.id}
|
||||
StartIcon={Edit2}
|
||||
onClick={() => router.push("/event-types/" + type.id)}>
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
</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>
|
||||
</Dropdown>
|
||||
</ButtonGroup>
|
||||
@@ -453,7 +527,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<div className="min-w-9 mx-5 flex sm:hidden">
|
||||
<Dropdown>
|
||||
<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>
|
||||
<DropdownMenuPortal>
|
||||
<DropdownMenuContent>
|
||||
@@ -461,7 +535,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<DropdownItem
|
||||
href={calLink}
|
||||
target="_blank"
|
||||
StartIcon={FiExternalLink}
|
||||
StartIcon={ExternalLink}
|
||||
className="w-full rounded-none">
|
||||
{t("preview")}
|
||||
</DropdownItem>
|
||||
@@ -473,7 +547,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
navigator.clipboard.writeText(calLink);
|
||||
showToast(t("link_copied"), "success");
|
||||
}}
|
||||
StartIcon={FiClipboard}
|
||||
StartIcon={Clipboard}
|
||||
className="w-full rounded-none text-left">
|
||||
{t("copy_link")}
|
||||
</DropdownItem>
|
||||
@@ -492,7 +566,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
.then(() => showToast(t("link_shared"), "success"))
|
||||
.catch(() => showToast(t("failed"), "error"));
|
||||
}}
|
||||
StartIcon={FiUpload}
|
||||
StartIcon={Upload}
|
||||
className="w-full rounded-none">
|
||||
{t("share")}
|
||||
</DropdownItem>
|
||||
@@ -501,7 +575,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => router.push("/event-types/" + type.id)}
|
||||
StartIcon={FiEdit}
|
||||
StartIcon={Edit}
|
||||
className="w-full rounded-none">
|
||||
{t("edit")}
|
||||
</DropdownItem>
|
||||
@@ -509,7 +583,7 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<DropdownMenuItem className="outline-none">
|
||||
<DropdownItem
|
||||
onClick={() => openDuplicateModal(type, group)}
|
||||
StartIcon={FiCopy}
|
||||
StartIcon={Copy}
|
||||
data-testid={"event-type-duplicate-" + type.id}>
|
||||
{t("duplicate")}
|
||||
</DropdownItem>
|
||||
@@ -521,8 +595,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
onClick={() => {
|
||||
setDeleteDialogOpen(true);
|
||||
setDeleteDialogTypeId(type.id);
|
||||
setDeleteDialogSchedulingType(type.schedulingType);
|
||||
}}
|
||||
StartIcon={FiTrash}
|
||||
StartIcon={Trash}
|
||||
className="w-full rounded-none">
|
||||
{t("delete")}
|
||||
</DropdownItem>
|
||||
@@ -539,14 +614,37 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||
<ConfirmationDialogContent
|
||||
variety="danger"
|
||||
title={t("delete_event_type")}
|
||||
confirmBtnText={t("confirm_delete_event_type")}
|
||||
loadingText={t("confirm_delete_event_type")}
|
||||
title={t(
|
||||
`delete_${deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"}_event_type`
|
||||
)}
|
||||
confirmBtnText={t(
|
||||
`confirm_delete_${
|
||||
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||
}_event_type`
|
||||
)}
|
||||
loadingText={t(
|
||||
`confirm_delete_${
|
||||
deleteDialogTypeSchedulingType === SchedulingType.MANAGED && "managed"
|
||||
}_event_type`
|
||||
)}
|
||||
onConfirm={(e) => {
|
||||
e.preventDefault();
|
||||
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>
|
||||
</Dialog>
|
||||
</div>
|
||||
@@ -589,7 +687,7 @@ const EventTypeListHeading = ({
|
||||
<span className="text-subtle ms-2 me-2 relative -top-px text-xs">
|
||||
<Link href={`/settings/teams/${teamId}/members`}>
|
||||
<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}
|
||||
</Badge>
|
||||
</Link>
|
||||
@@ -617,7 +715,7 @@ const CreateFirstEventTypeView = () => {
|
||||
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon={FiLink}
|
||||
Icon={LinkIcon}
|
||||
headline={t("new_event_type_heading")}
|
||||
description={t("new_event_type_description")}
|
||||
/>
|
||||
@@ -638,6 +736,7 @@ const CTA = () => {
|
||||
teamId: profile.teamId,
|
||||
label: profile.name || profile.slug,
|
||||
image: profile.image,
|
||||
membershipRole: profile.membershipRole,
|
||||
slug: profile.slug,
|
||||
};
|
||||
});
|
||||
@@ -646,7 +745,7 @@ const CTA = () => {
|
||||
<CreateButton
|
||||
subtitle={t("create_event_on").toUpperCase()}
|
||||
options={profileOptions}
|
||||
createDialog={CreateEventTypeDialog}
|
||||
createDialog={() => <CreateEventTypeDialog profileOptions={profileOptions} />}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -15,7 +15,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc";
|
||||
import { Button, ButtonGroup } from "@calcom/ui";
|
||||
import { FiRefreshCcw, FiUserPlus, FiUsers } from "@calcom/ui/components/icon";
|
||||
import { RefreshCcw, UserPlus, Users } from "@calcom/ui/components/icon";
|
||||
|
||||
const Heading = () => {
|
||||
const { t } = useLocale();
|
||||
@@ -35,17 +35,17 @@ export default function InsightsPage() {
|
||||
const { data: user } = trpc.viewer.me.useQuery();
|
||||
const features = [
|
||||
{
|
||||
icon: <FiUsers className="h-5 w-5" />,
|
||||
icon: <Users className="h-5 w-5" />,
|
||||
title: t("view_bookings_across"),
|
||||
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"),
|
||||
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"),
|
||||
description: t("spot_popular_event_types_description"),
|
||||
},
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-edit-view";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-add-view";
|
||||
@@ -0,0 +1 @@
|
||||
export { default } from "@calcom/features/ee/users/pages/users-listing-view";
|
||||
@@ -6,7 +6,7 @@ import { classNames } from "@calcom/lib";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, Meta } from "@calcom/ui";
|
||||
import { FiExternalLink } from "@calcom/ui/components/icon";
|
||||
import { ExternalLink } from "@calcom/ui/components/icon";
|
||||
|
||||
interface CtaRowProps {
|
||||
title: string;
|
||||
@@ -48,7 +48,7 @@ const BillingView = () => {
|
||||
<CtaRow
|
||||
title={t("billing_manage_details_title")}
|
||||
description={t("billing_manage_details_description")}>
|
||||
<Button color="primary" href={billingHref} target="_blank" EndIcon={FiExternalLink}>
|
||||
<Button color="primary" href={billingHref} target="_blank" EndIcon={ExternalLink}>
|
||||
{t("billing_portal")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
|
||||
@@ -16,7 +16,7 @@ import {
|
||||
Meta,
|
||||
AppSkeletonLoader as SkeletonLoader,
|
||||
} from "@calcom/ui";
|
||||
import { FiLink, FiPlus } from "@calcom/ui/components/icon";
|
||||
import { Link as LinkIcon, Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
const ApiKeysView = () => {
|
||||
const { t } = useLocale();
|
||||
@@ -32,7 +32,7 @@ const ApiKeysView = () => {
|
||||
return (
|
||||
<Button
|
||||
color="secondary"
|
||||
StartIcon={FiPlus}
|
||||
StartIcon={Plus}
|
||||
onClick={() => {
|
||||
setApiKeyToEdit(undefined);
|
||||
setApiKeyModal(true);
|
||||
@@ -72,7 +72,7 @@ const ApiKeysView = () => {
|
||||
</>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={FiLink}
|
||||
Icon={LinkIcon}
|
||||
headline={t("create_first_api_key")}
|
||||
description={t("create_first_api_key_description", { appName: APP_NAME })}
|
||||
buttonRaw={<NewApiKeyButton />}
|
||||
|
||||
@@ -181,7 +181,7 @@ const AppearanceView = () => {
|
||||
{/* TODO future PR to preview brandColors */}
|
||||
{/* <Button
|
||||
color="secondary"
|
||||
EndIcon={FiExternalLink}
|
||||
EndIcon={ExternalLink}
|
||||
className="mt-6"
|
||||
onClick={() => window.open(`${WEBAPP_URL}/${user.username}/${user.eventTypes[0].title}`, "_blank")}>
|
||||
Preview
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SkeletonText,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus, FiCalendar } from "@calcom/ui/components/icon";
|
||||
import { Plus, Calendar } from "@calcom/ui/components/icon";
|
||||
|
||||
import { QueryCell } from "@lib/QueryCell";
|
||||
|
||||
@@ -50,7 +50,7 @@ const AddCalendarButton = () => {
|
||||
|
||||
return (
|
||||
<>
|
||||
<Button color="secondary" StartIcon={FiPlus} href="/apps/categories/calendar">
|
||||
<Button color="secondary" StartIcon={Plus} href="/apps/categories/calendar">
|
||||
{t("add_calendar")}
|
||||
</Button>
|
||||
</>
|
||||
@@ -87,7 +87,7 @@ const CalendarsView = () => {
|
||||
<div>
|
||||
<div className="bg-muted border-subtle mt-4 flex space-x-4 rounded-md p-2 sm:mx-0 sm:p-10 md:border md:p-6 xl:mt-0">
|
||||
<div className=" bg-default border-subtle flex h-9 w-9 items-center justify-center rounded-md border-2 p-[6px]">
|
||||
<FiCalendar className="text-default h-6 w-6" />
|
||||
<Calendar className="text-default h-6 w-6" />
|
||||
</div>
|
||||
|
||||
<div className="flex w-full flex-col space-y-3">
|
||||
@@ -198,7 +198,7 @@ const CalendarsView = () => {
|
||||
</div>
|
||||
) : (
|
||||
<EmptyScreen
|
||||
Icon={FiCalendar}
|
||||
Icon={Calendar}
|
||||
headline={t("no_calendar_installed")}
|
||||
description={t("no_calendar_installed_description")}
|
||||
buttonText={t("add_a_calendar")}
|
||||
|
||||
@@ -24,7 +24,7 @@ import {
|
||||
SkeletonContainer,
|
||||
SkeletonText,
|
||||
} from "@calcom/ui";
|
||||
import { FiAlertCircle, FiMoreHorizontal, FiTrash, FiVideo } from "@calcom/ui/components/icon";
|
||||
import { AlertCircle, MoreHorizontal, Trash, Video } from "@calcom/ui/components/icon";
|
||||
|
||||
import AppListCard from "@components/AppListCard";
|
||||
|
||||
@@ -111,7 +111,7 @@ const ConferencingLayout = () => {
|
||||
<div>
|
||||
<Dropdown>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button StartIcon={FiMoreHorizontal} variant="icon" color="secondary" />
|
||||
<Button StartIcon={MoreHorizontal} variant="icon" color="secondary" />
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent>
|
||||
{!appIsDefault && (
|
||||
@@ -119,7 +119,7 @@ const ConferencingLayout = () => {
|
||||
<DropdownItem
|
||||
type="button"
|
||||
color="secondary"
|
||||
StartIcon={FiVideo}
|
||||
StartIcon={Video}
|
||||
onClick={() => {
|
||||
const locationType = getEventLocationTypeFromApp(
|
||||
app?.locationOption?.value ?? ""
|
||||
@@ -141,7 +141,7 @@ const ConferencingLayout = () => {
|
||||
type="button"
|
||||
color="destructive"
|
||||
disabled={app.isGlobal}
|
||||
StartIcon={FiTrash}
|
||||
StartIcon={Trash}
|
||||
onClick={() => {
|
||||
setDeleteCredentialId(app.credentialIds[0]);
|
||||
setDeleteAppModal(true);
|
||||
@@ -163,7 +163,7 @@ const ConferencingLayout = () => {
|
||||
title={t("Remove app")}
|
||||
description={t("are_you_sure_you_want_to_remove_this_app")}
|
||||
type="confirmation"
|
||||
Icon={FiAlertCircle}>
|
||||
Icon={AlertCircle}>
|
||||
<DialogFooter>
|
||||
<Button color="primary" onClick={() => deleteAppMutation.mutate({ id: deleteCredentialId })}>
|
||||
{t("yes_remove_app")}
|
||||
|
||||
@@ -38,7 +38,7 @@ import {
|
||||
TextField,
|
||||
Editor,
|
||||
} from "@calcom/ui";
|
||||
import { FiAlertTriangle, FiTrash2 } from "@calcom/ui/components/icon";
|
||||
import { AlertTriangle, Trash2 } from "@calcom/ui/components/icon";
|
||||
|
||||
import TwoFactor from "@components/auth/TwoFactor";
|
||||
import { UsernameAvailabilityField } from "@components/ui/UsernameAvailability";
|
||||
@@ -232,7 +232,7 @@ const ProfileView = () => {
|
||||
{/* Delete account Dialog */}
|
||||
<Dialog open={deleteAccountOpen} onOpenChange={setDeleteAccountOpen}>
|
||||
<DialogTrigger asChild>
|
||||
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={FiTrash2}>
|
||||
<Button data-testid="delete-account" color="destructive" className="mt-1" StartIcon={Trash2}>
|
||||
{t("delete_account")}
|
||||
</Button>
|
||||
</DialogTrigger>
|
||||
@@ -240,7 +240,7 @@ const ProfileView = () => {
|
||||
title={t("delete_account_modal_title")}
|
||||
description={t("confirm_delete_account_modal", { appName: APP_NAME })}
|
||||
type="creation"
|
||||
Icon={FiAlertTriangle}>
|
||||
Icon={AlertTriangle}>
|
||||
<>
|
||||
<p className="text-default mb-7">
|
||||
{t("delete_account_confirmation_message", { appName: APP_NAME })}
|
||||
@@ -283,7 +283,7 @@ const ProfileView = () => {
|
||||
title={t("confirm_password")}
|
||||
description={t("confirm_password_change_email")}
|
||||
type="creation"
|
||||
Icon={FiAlertTriangle}>
|
||||
Icon={AlertTriangle}>
|
||||
<>
|
||||
<PasswordField
|
||||
data-testid="password"
|
||||
|
||||
@@ -15,7 +15,7 @@ import { getTeamWithMembers } from "@calcom/lib/server/queries/teams";
|
||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||
import prisma from "@calcom/prisma";
|
||||
import { Avatar, AvatarGroup, Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { FiArrowRight } from "@calcom/ui/components/icon";
|
||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
@@ -55,12 +55,12 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
}
|
||||
|
||||
const EventTypes = () => (
|
||||
<ul className="dark:border-darkgray-300 border-subtle rounded-md border">
|
||||
<ul className="border-subtle border-subtle rounded-md border">
|
||||
{team.eventTypes.map((type, index) => (
|
||||
<li
|
||||
key={index}
|
||||
className={classNames(
|
||||
"dark:bg-darkgray-100 dark:border-darkgray-300 bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
|
||||
"dark:bg-darkgray-100 border-subtle bg-default hover:bg-muted border-subtle group relative border-b first:rounded-t-md last:rounded-b-md last:border-b-0",
|
||||
!isEmbed && "bg-default"
|
||||
)}>
|
||||
<div className="px-6 py-4 ">
|
||||
@@ -125,7 +125,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<div>
|
||||
<div className="relative mt-12">
|
||||
<div className="absolute inset-0 flex items-center" aria-hidden="true">
|
||||
<div className="dark:border-darkgray-300 border-subtle w-full border-t" />
|
||||
<div className="border-subtle border-subtle w-full border-t" />
|
||||
</div>
|
||||
<div className="relative flex justify-center">
|
||||
<span className="dark:bg-darkgray-50 bg-subtle text-subtle dark:text-inverted px-2 text-sm">
|
||||
@@ -137,7 +137,7 @@ function TeamPage({ team, isUnpublished }: TeamPageProps) {
|
||||
<aside className="dark:text-inverted mt-8 flex justify-center text-center">
|
||||
<Button
|
||||
color="minimal"
|
||||
EndIcon={FiArrowRight}
|
||||
EndIcon={ArrowRight}
|
||||
className="dark:hover:bg-darkgray-200"
|
||||
href={`/team/${team.slug}?members=1`}
|
||||
shallow={true}>
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { serverSideTranslations } from "next-i18next/serverSideTranslations";
|
||||
|
||||
import { TeamsListing } from "@calcom/features/ee/teams/components";
|
||||
import Shell from "@calcom/features/shell/Shell";
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button } from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
import { Plus } from "@calcom/ui/components/icon";
|
||||
|
||||
function Teams() {
|
||||
const { t } = useLocale();
|
||||
@@ -14,7 +16,7 @@ function Teams() {
|
||||
CTA={
|
||||
<Button
|
||||
variant="fab"
|
||||
StartIcon={FiPlus}
|
||||
StartIcon={Plus}
|
||||
type="button"
|
||||
href={`${WEBAPP_URL}/settings/teams/new?returnTo=${WEBAPP_URL}/teams`}>
|
||||
{t("new")}
|
||||
@@ -25,6 +27,14 @@ function Teams() {
|
||||
);
|
||||
}
|
||||
|
||||
export const getStaticProps = async () => {
|
||||
return {
|
||||
props: {
|
||||
...(await serverSideTranslations("en", ["common"])),
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
Teams.requiresLicense = false;
|
||||
|
||||
export default Teams;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import type { DailyEventObjectRecordingStarted } from "@daily-co/daily-js";
|
||||
import DailyIframe from "@daily-co/daily-js";
|
||||
import MarkdownIt from "markdown-it";
|
||||
import type { GetServerSidePropsContext } from "next";
|
||||
import Head from "next/head";
|
||||
import { useState, useEffect, useRef } from "react";
|
||||
import z from "zod";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||
@@ -12,16 +14,23 @@ import { formatToLocalizedDate, formatToLocalizedTime } from "@calcom/lib/date-f
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { FiChevronRight } from "@calcom/ui/components/icon";
|
||||
import { ChevronRight } from "@calcom/ui/components/icon";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
const recordingStartedEventResponse = z
|
||||
.object({
|
||||
recordingId: z.string(),
|
||||
})
|
||||
.passthrough();
|
||||
|
||||
export type JoinCallPageProps = inferSSRProps<typeof getServerSideProps>;
|
||||
const md = new MarkdownIt("default", { html: true, breaks: true, linkify: true });
|
||||
|
||||
export default function JoinCall(props: JoinCallPageProps) {
|
||||
const { t } = useLocale();
|
||||
const { meetingUrl, meetingPassword, booking } = props;
|
||||
const recordingId = useRef<string | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const callFrame = DailyIframe.createFrame({
|
||||
@@ -49,11 +58,30 @@ export default function JoinCall(props: JoinCallPageProps) {
|
||||
...(typeof meetingPassword === "string" && { token: meetingPassword }),
|
||||
});
|
||||
callFrame.join();
|
||||
callFrame.on("recording-started", onRecordingStarted).on("recording-stopped", onRecordingStopped);
|
||||
return () => {
|
||||
callFrame.destroy();
|
||||
};
|
||||
}, []);
|
||||
|
||||
const onRecordingStopped = () => {
|
||||
const data = { recordingId: recordingId.current, bookingUID: booking.uid };
|
||||
|
||||
fetch("/api/recorded-daily-video", {
|
||||
method: "POST",
|
||||
body: JSON.stringify(data),
|
||||
}).catch((err) => {
|
||||
console.log(err);
|
||||
});
|
||||
|
||||
recordingId.current = null;
|
||||
};
|
||||
|
||||
const onRecordingStarted = (event?: DailyEventObjectRecordingStarted | undefined) => {
|
||||
const response = recordingStartedEventResponse.parse(event);
|
||||
recordingId.current = response.recordingId;
|
||||
};
|
||||
|
||||
const title = `${APP_NAME} Video`;
|
||||
return (
|
||||
<>
|
||||
@@ -222,7 +250,7 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
||||
aria-label={`${open ? "close" : "open"} booking description sidebar`}
|
||||
className="h-20 w-6 rounded-r-md border border-l-0 border-gray-300/20 bg-black/60 text-white shadow-sm backdrop-blur-lg"
|
||||
onClick={() => setOpen(!open)}>
|
||||
<FiChevronRight
|
||||
<ChevronRight
|
||||
aria-hidden
|
||||
className={classNames(open && "rotate-180", "w-5 transition-all duration-300 ease-in-out")}
|
||||
/>
|
||||
@@ -246,6 +274,7 @@ export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||
...bookingMinimalSelect,
|
||||
uid: true,
|
||||
description: true,
|
||||
isRecorded: true,
|
||||
user: {
|
||||
select: {
|
||||
id: true,
|
||||
|
||||
@@ -5,7 +5,7 @@ import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import { Button, HeadSeo } from "@calcom/ui";
|
||||
import { FiArrowRight, FiCalendar, FiX } from "@calcom/ui/components/icon";
|
||||
import { ArrowRight, Calendar, X } from "@calcom/ui/components/icon";
|
||||
|
||||
import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
@@ -29,7 +29,7 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
|
||||
aria-labelledby="modal-headline">
|
||||
<div>
|
||||
<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 className="mt-3 text-center sm:mt-5">
|
||||
<h3 className="text-emphasis text-lg font-medium leading-6" id="modal-headline">
|
||||
@@ -41,14 +41,14 @@ export default function MeetingUnavailable(props: inferSSRProps<typeof getServer
|
||||
{props.booking.title}
|
||||
</h2>
|
||||
<p className="text-subtle text-center">
|
||||
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<Calendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-5 text-center sm:mt-6">
|
||||
<div className="mt-5">
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
@@ -6,7 +6,7 @@ import { detectBrowserTimeFormat } from "@calcom/lib/timeFormat";
|
||||
import prisma, { bookingMinimalSelect } from "@calcom/prisma";
|
||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||
import { Button, HeadSeo, EmptyScreen } from "@calcom/ui";
|
||||
import { FiArrowRight, FiCalendar, FiClock } from "@calcom/ui/components/icon";
|
||||
import { ArrowRight, Calendar, Clock } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function MeetingNotStarted(props: inferSSRProps<typeof getServerSideProps>) {
|
||||
const { t } = useLocale();
|
||||
@@ -15,19 +15,19 @@ export default function MeetingNotStarted(props: inferSSRProps<typeof getServerS
|
||||
<HeadSeo title={t("this_meeting_has_not_started_yet")} description={props.booking.title} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
Icon={Clock}
|
||||
headline={t("this_meeting_has_not_started_yet")}
|
||||
description={
|
||||
<>
|
||||
<h2 className="mb-2 text-center font-medium">{props.booking.title}</h2>
|
||||
<p className="text-subtle text-center">
|
||||
<FiCalendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
<Calendar className="mr-1 -mt-1 inline-block h-4 w-4" />
|
||||
{dayjs(props.booking.startTime).format(detectBrowserTimeFormat + ", dddd DD MMMM YYYY")}
|
||||
</p>
|
||||
</>
|
||||
}
|
||||
buttonRaw={
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
|
||||
{t("go_back")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { Button, EmptyScreen, HeadSeo } from "@calcom/ui";
|
||||
import { FiX, FiArrowRight } from "@calcom/ui/components/icon";
|
||||
import { X, ArrowRight } from "@calcom/ui/components/icon";
|
||||
|
||||
export default function NoMeetingFound() {
|
||||
const { t } = useLocale();
|
||||
@@ -10,11 +10,11 @@ export default function NoMeetingFound() {
|
||||
<HeadSeo title={t("no_meeting_found")} description={t("no_meeting_found")} />
|
||||
<main className="mx-auto my-24 max-w-3xl">
|
||||
<EmptyScreen
|
||||
Icon={FiX}
|
||||
Icon={X}
|
||||
headline={t("no_meeting_found")}
|
||||
description={t("no_meeting_found_description")}
|
||||
buttonRaw={
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={FiArrowRight}>
|
||||
<Button data-testid="return-home" href="/event-types" EndIcon={ArrowRight}>
|
||||
{t("go_back_home")}
|
||||
</Button>
|
||||
}
|
||||
|
||||
@@ -268,6 +268,9 @@ const createUserFixture = (user: UserWithIncludes, page: Page) => {
|
||||
routingForms: user.routingForms,
|
||||
self,
|
||||
login: async () => login({ ...(await self()), password: user.username }, store.page),
|
||||
logout: async () => {
|
||||
await page.goto("/auth/logout");
|
||||
},
|
||||
getPaymentCredential: async () => getPaymentCredential(store.page),
|
||||
// ths is for developemnt only aimed to inject debugging messages in the metadata field of the user
|
||||
debug: async (message: string | Record<string, JSONValue>) => {
|
||||
@@ -333,9 +336,8 @@ export async function login(
|
||||
await passwordLocator.fill(user.password ?? user.username!);
|
||||
await signInLocator.click();
|
||||
|
||||
// 2 seconds of delay to give the session enough time for a clean load
|
||||
// eslint-disable-next-line playwright/no-wait-for-timeout
|
||||
await page.waitForTimeout(2000);
|
||||
// Moving away from waiting 2 seconds, as it is not a reliable way to expect session to be started
|
||||
await page.waitForLoadState("networkidle");
|
||||
}
|
||||
|
||||
export async function getPaymentCredential(page: Page) {
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import { expect } from "@playwright/test";
|
||||
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
test.afterEach(({ users }) => users.deleteAll());
|
||||
|
||||
test.describe("Managed Event Types tests", () => {
|
||||
test("Can create managed event type", async ({ page, users }) => {
|
||||
// Creating the owner user of the team
|
||||
const adminUser = await users.create();
|
||||
// Creating the member user of the team
|
||||
const memberUser = await users.create();
|
||||
// First we work with owner user, logging in
|
||||
await adminUser.login();
|
||||
// Making sure page loads completely
|
||||
await page.waitForLoadState("networkidle");
|
||||
// Let's create a team
|
||||
await page.goto("/teams");
|
||||
|
||||
await test.step("Managed event option exists for team admin", async () => {
|
||||
// Proceed to create a team
|
||||
await page.locator("text=Create Team").click();
|
||||
await page.waitForURL("/settings/teams/new");
|
||||
|
||||
// Filling team creation form wizard
|
||||
await page.locator('input[name="name"]').fill(`${adminUser.username}'s Team`);
|
||||
await page.locator("text=Continue").click();
|
||||
await page.waitForURL(/\/settings\/teams\/(\d+)\/onboard-members$/i);
|
||||
await page.locator('[data-testid="new-member-button"]').click();
|
||||
await page.locator('[placeholder="email\\@example\\.com"]').fill(`${memberUser.username}@example.com`);
|
||||
await page.locator('[data-testid="invite-new-member-button"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator("text=Publish team").click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
|
||||
// Going to create an event type
|
||||
await page.goto("/event-types");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.click("[data-testid=new-event-type-dropdown]");
|
||||
await page.click("[data-testid=option-team-1]");
|
||||
// Expecting we can add a managed event type as team owner
|
||||
await expect(page.locator('input[value="MANAGED"]')).toBeVisible();
|
||||
|
||||
// Actually creating a managed event type to test things further
|
||||
await page.click('input[value="MANAGED"]');
|
||||
await page.fill("[name=title]", "managed");
|
||||
await page.click("[type=submit]");
|
||||
});
|
||||
|
||||
await test.step("Managed event type has unlocked fields for admin", async () => {
|
||||
await page.waitForSelector('[data-testid="update-eventtype"]');
|
||||
await expect(page.locator('input[name="title"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).toBeEditable();
|
||||
});
|
||||
|
||||
await test.step("Managed event type exists for added member", async () => {
|
||||
// Now we need to accept the invitation as member and come back in as admin to
|
||||
// assign the member in the managed event type
|
||||
await adminUser.logout();
|
||||
await memberUser.login();
|
||||
await page.waitForSelector('[data-testid="event-types"]');
|
||||
await page.goto("/teams");
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator('button[data-testid^="accept-invitation"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await memberUser.logout();
|
||||
|
||||
// Coming back as team owner to assign member user to managed event
|
||||
await adminUser.login();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await page.locator('[data-testid="event-types"] a[title="managed"]').click();
|
||||
await page.locator('[data-testid="vertical-tab-assignment"]').click();
|
||||
await page.locator('[class$="control"]').filter({ hasText: "Select..." }).click();
|
||||
await page.locator("#react-select-5-option-1").click();
|
||||
await page.locator('[type="submit"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await adminUser.logout();
|
||||
|
||||
// Coming back as member user to see if there is a managed event present after assignment
|
||||
await memberUser.login();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('[data-testid="event-types"] a[title="managed"]')).toBeVisible();
|
||||
});
|
||||
|
||||
await test.step("Managed event type has locked fields for added member", async () => {
|
||||
page.locator('[data-testid="event-types"] a[title="managed"]').click();
|
||||
await page.waitForLoadState("networkidle");
|
||||
await expect(page.locator('input[name="title"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="slug"]')).not.toBeEditable();
|
||||
await expect(page.locator('input[name="length"]')).not.toBeEditable();
|
||||
});
|
||||
});
|
||||
});
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.0 KiB |
@@ -0,0 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="#fff" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-arrow-right"><line x1="5" y1="12" x2="19" y2="12"></line><polyline points="12 5 19 12 12 19"></polyline></svg>
|
||||
|
After Width: | Height: | Size: 307 B |
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "الحد الأدنى من الوقت للحجز",
|
||||
"slot_interval": "الفترات الزمنية بين عمليات الحجز",
|
||||
"slot_interval_default": "استخدام طول الحدث (الوضع الافتراضي)",
|
||||
"delete_event_type_description": "هل تريد بالتأكيد حذف نوع الحدث هذا؟ أي شخص شاركت هذا الرابط معه لن يستطيع بعد الآن الحجز باستخدامه.",
|
||||
"delete_event_type": "حذف نوع الحدث",
|
||||
"confirm_delete_event_type": "نعم، احذف نوع الحدث",
|
||||
"delete_account": "حذف الحساب",
|
||||
"confirm_delete_account": "نعم، احذف الحساب",
|
||||
"delete_account_confirmation_message": "هل تريد بالتأكيد حذف حسابك على {{appName}}؟ أي شخص شاركت رابط حسابك معه لن يستطيع بعد الآن الحجز باستخدامه، وستفقد أي تفضيلات حفظتها.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "رمز التحقق",
|
||||
"can_you_try_again": "هلّا جربت المحاولة ثانيةً في وقت مختلف؟",
|
||||
"verify": "التحقق",
|
||||
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
|
||||
"select_all": "اختيار الكل",
|
||||
"default_conferencing_bulk_title": "التحديث الجماعي لمجموعة أنواع الفعاليات الحالية",
|
||||
"invalid_event_name_variables": "ثمة متغيّر غير غير صالح في اسم فعاليتك",
|
||||
"default_conferencing_bulk_description": "تحديث المواقع لأنواع الفعاليات المحددة"
|
||||
}
|
||||
|
||||
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "Minimální potřebná doba",
|
||||
"slot_interval": "Časové intervaly",
|
||||
"slot_interval_default": "Použít délku události (výchozí)",
|
||||
"delete_event_type_description": "Opravdu chcete smazat tento typ události? Všichni, se kterými jsme tento odkaz sdíleli, ho nebudou moct používat pro rezervování.",
|
||||
"delete_event_type": "Smazat typ události",
|
||||
"confirm_delete_event_type": "Ano, smazat typ události",
|
||||
"delete_account": "Smazat účet",
|
||||
"confirm_delete_account": "Ano, smazat účet",
|
||||
"delete_account_confirmation_message": "Opravdu chcete odstranit svůj účet na {{appName}}? Všichni, se kterými jste sdíleli odkaz na účet, přijdou o možnost vytvářet rezervace a vy přijdete o uložené předvolby.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "Ověřovací kód",
|
||||
"can_you_try_again": "Můžete to zkusit znovu a použít jiný čas?",
|
||||
"verify": "Ověřit",
|
||||
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
|
||||
"select_all": "Vybrat vše",
|
||||
"default_conferencing_bulk_title": "Hromadná aktualizace stávajících typů událostí",
|
||||
"invalid_event_name_variables": "V názvu události je neplatná proměnná",
|
||||
"default_conferencing_bulk_description": "Aktualizujte místa pro vybrané typy událostí"
|
||||
}
|
||||
|
||||
@@ -733,9 +733,6 @@
|
||||
"minimum_booking_notice": "Minimum Varsel",
|
||||
"slot_interval": "Tidsrums intervaller",
|
||||
"slot_interval_default": "Brug begivenhedslængde (standard)",
|
||||
"delete_event_type_description": "Er du sikker på, at du vil slette denne begivenhedstype? Alle, som du har delt dette link med, vil ikke længere kunne booke ved hjælp af det.",
|
||||
"delete_event_type": "Slet Begivenhedstype",
|
||||
"confirm_delete_event_type": "Ja, slet begivenhedstype",
|
||||
"delete_account": "Slet konto",
|
||||
"confirm_delete_account": "Ja, slet konto",
|
||||
"delete_account_confirmation_message": "Er du sikker på, at du vil slette din {{appName}} konto? Alle, som du har delt dit konto-link med, vil ikke længere kunne booke ved hjælp af det, og eventuelle præferencer du har gemt, vil gå tabt.",
|
||||
|
||||
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "Mindesvorlaufzeit für eine Buchung",
|
||||
"slot_interval": "Zeit-Slot-Intervalle",
|
||||
"slot_interval_default": "Verwende Terminlänge (Standard)",
|
||||
"delete_event_type_description": "Sind Sie sicher, dass Sie diesen Termintypen löschen möchten? Empfänger dieses Links können dann nicht mehr buchen.",
|
||||
"delete_event_type": "Termintyp löschen",
|
||||
"confirm_delete_event_type": "Ja, Termintyp löschen",
|
||||
"delete_account": "Account löschen",
|
||||
"confirm_delete_account": "Ja, Account löschen",
|
||||
"delete_account_confirmation_message": "Sind Sie sicher, dass Sie Ihr {{appName}}-Konto löschen möchten? Sie können dann nicht mehr über Ihre geteilten Links gebucht werden und alle Einstellungen werden gelöscht.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "Bestätigungscode",
|
||||
"can_you_try_again": "Können Sie es zu einem anderen Zeitpunkt erneut versuchen?",
|
||||
"verify": "Verifizieren",
|
||||
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
|
||||
"select_all": "Alle auswählen",
|
||||
"default_conferencing_bulk_title": "Massenaktualisierung vorhandener Termintypen",
|
||||
"invalid_event_name_variables": "Es gibt eine ungültige Variable in Ihrem Terminnamen",
|
||||
"default_conferencing_bulk_description": "Orte für die ausgewählten Termintypen aktualisieren"
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
{
|
||||
"identity_provider": "Identity provider",
|
||||
"trial_days_left": "You have $t(day, {\"count\": {{days}} }) left on your PRO trial",
|
||||
"day_one": "{{count}} day",
|
||||
"day_other": "{{count}} days",
|
||||
@@ -69,6 +70,8 @@
|
||||
"event_awaiting_approval_subject": "Awaiting Approval: {{title}} at {{date}}",
|
||||
"event_still_awaiting_approval": "An event is still waiting for your approval",
|
||||
"booking_submitted_subject": "Booking Submitted: {{title}} at {{date}}",
|
||||
"download_recording_subject": "Download Recording: {{title}} at {{date}}",
|
||||
"download_your_recording": "Download your recording",
|
||||
"your_meeting_has_been_booked": "Your meeting has been booked",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Your {{title}} has been rescheduled to {{date}}.",
|
||||
"event_has_been_rescheduled": "Updated - Your event has been rescheduled",
|
||||
@@ -224,6 +227,7 @@
|
||||
"done": "Done",
|
||||
"all_done": "All done!",
|
||||
"all_apps": "All",
|
||||
"available_apps": "Available Apps",
|
||||
"check_email_reset_password": "Check your email. We sent you a link to reset your password.",
|
||||
"finish": "Finish",
|
||||
"few_sentences_about_yourself": "A few sentences about yourself. This will appear on your personal url page.",
|
||||
@@ -586,6 +590,20 @@
|
||||
"minutes": "Minutes",
|
||||
"round_robin": "Round Robin",
|
||||
"round_robin_description": "Cycle meetings between multiple team members.",
|
||||
"managed_event": "Managed Event",
|
||||
"username_placeholder": "username",
|
||||
"managed_event_description": "Create & distribute event types in bulk to team members",
|
||||
"managed": "Managed",
|
||||
"managed_event_url_clarification": "\"username\" will be filled by the username of the members assigned",
|
||||
"assign_to": "Assign to",
|
||||
"add_members": "Add members...",
|
||||
"count_members_one": "{{count}} member",
|
||||
"count_members_other": "{{count}} members",
|
||||
"no_assigned_members": "No assigned members",
|
||||
"assigned_to": "Assigned to",
|
||||
"start_assigning_members_above": "Start assigning members above",
|
||||
"locked_fields_admin_description": "Members will not be able to edit this",
|
||||
"locked_fields_member_description": "This option was locked by the team admin",
|
||||
"url": "URL",
|
||||
"hidden": "Hidden",
|
||||
"readonly": "Readonly",
|
||||
@@ -737,9 +755,11 @@
|
||||
"minimum_booking_notice": "Minimum Notice",
|
||||
"slot_interval": "Time-slot intervals",
|
||||
"slot_interval_default": "Use event length (default)",
|
||||
"delete_event_type_description": "Are you sure you want to delete this event type? Anyone who you've shared this link with will no longer be able to book using it.",
|
||||
"delete_event_type": "Delete Event Type",
|
||||
"confirm_delete_event_type": "Yes, delete event type",
|
||||
"delete_event_type": "Delete event type?",
|
||||
"delete_managed_event_type": "Delete managed event type?",
|
||||
"delete_event_type_description": "Anyone who you've shared this link with will no longer be able to book using it.",
|
||||
"delete_managed_event_type_description": "<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>",
|
||||
"confirm_delete_event_type": "Yes, delete",
|
||||
"delete_account": "Delete account",
|
||||
"confirm_delete_account": "Yes, delete account",
|
||||
"delete_account_confirmation_message": "Are you sure you want to delete your {{appName}} account? Anyone who you've shared your account link with will no longer be able to book using it and any preferences you have saved will be lost.",
|
||||
@@ -1182,6 +1202,7 @@
|
||||
"start_of_week": "Start of week",
|
||||
"recordings_title": "Recordings",
|
||||
"recording": "Recording",
|
||||
"happy_scheduling": "Happy Scheduling",
|
||||
"select_calendars": "Select which calendars you want to check for conflicts to prevent double bookings.",
|
||||
"check_for_conflicts": "Check for conflicts",
|
||||
"view_recordings": "View recordings",
|
||||
@@ -1260,6 +1281,8 @@
|
||||
"download_responses": "Download responses",
|
||||
"download_responses_description": "Download all responses to your form in CSV format.",
|
||||
"download": "Download",
|
||||
"download_recording": "Download Recording",
|
||||
"recording_from_your_recent_call":"A recording from your recent call on Cal.com is ready for download",
|
||||
"create_your_first_form": "Create your first form",
|
||||
"create_your_first_form_description": "With Routing Forms you can ask qualifying questions and route to the correct person or event type.",
|
||||
"create_your_first_webhook": "Create your first Webhook",
|
||||
@@ -1448,11 +1471,17 @@
|
||||
"fixed_round_robin": "Fixed round robin",
|
||||
"add_one_fixed_attendee": "Add one fixed attendee and round robin through a number of attendees.",
|
||||
"calcom_is_better_with_team": "Cal.com is better with teams",
|
||||
"the_calcom_team":"The Cal.com team",
|
||||
"add_your_team_members": "Add your team members to your event types. Use collective scheduling to include everyone or find the most suitable person with round robin scheduling.",
|
||||
"booking_limit_reached": "Booking Limit for this event type has been reached",
|
||||
"duration_limit_reached": "Duration Limit for this event type has been reached",
|
||||
"admin_has_disabled": "An admin has disabled {{appName}}",
|
||||
"disabled_app_affects_event_type": "An admin has disabled {{appName}} which affects your event type {{eventType}}",
|
||||
"event_replaced_notice": "An admin has replaced one of your event types",
|
||||
"email_subject_slug_replacement": "A team administrator has replaced your event /{{slug}}",
|
||||
"email_body_slug_replacement_notice": "An administrator on the <strong>{{teamName}}</strong> team has replaced your event type <strong>/{{slug}}</strong> with a managed event type that they control.",
|
||||
"email_body_slug_replacement_info": "Your link will continue to work but some settings for it may have changed. You can review it in event types.",
|
||||
"email_body_slug_replacement_suggestion": "If you have any questions about the event type, please reach out to your administrator.<br /><br />Happy scheduling, <br />The Cal.com team",
|
||||
"disable_payment_app": "The admin has disabled {{appName}} which affects your event type {{title}}. Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin renables your payment method.",
|
||||
"payment_disabled_still_able_to_book": "Attendees are still able to book this type of event but will not be prompted to pay. You may hide hide the event type to prevent this until your admin reenables your payment method.",
|
||||
"app_disabled_with_event_type": "The admin has disabled {{appName}} which affects your event type {{title}}.",
|
||||
@@ -1667,11 +1696,31 @@
|
||||
"verification_code": "Verification code",
|
||||
"can_you_try_again": "Can you try again with a different time?",
|
||||
"verify": "Verify",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"select_all": "Select All",
|
||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"members_default_schedule": "Member's default schedule",
|
||||
"set_by_admin": "Set by team admin",
|
||||
"members_default_location": "Member's default location",
|
||||
"members_default_schedule_description": "We will use each members default availability schedule. They will be able to edit or change it.",
|
||||
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"locked_for_members": "Locked for members",
|
||||
"locked_apps_description": "Members will be able to see the active apps but will not be able to edit any app settings",
|
||||
"locked_webhooks_description": "Members will be able to see the active webhooks but will not be able to edit any webhook settings",
|
||||
"locked_workflows_description": "Members will be able to see the active workflows but will not be able to edit any workflow settings",
|
||||
"locked_by_admin": "Locked by admin",
|
||||
"app_not_connected": "You have not connected a {{appName}} account.",
|
||||
"connect_now": "Connect now",
|
||||
"managed_event_dialog_confirm_button_one": "Replace & notify {{count}} member",
|
||||
"managed_event_dialog_confirm_button_other": "Replace & notify {{count}} members",
|
||||
"managed_event_dialog_title_one": "The url /{{slug}} already exists for {{count}} member. Do you want to replace it?",
|
||||
"managed_event_dialog_title_other": "The url /{{slug}} already exists for {{count}} members. Do you want to replace it?",
|
||||
"managed_event_dialog_information_one": "<strong>{{names}}</strong> is already using the <strong>/{{slug}}</strong> url.",
|
||||
"managed_event_dialog_information_other": "<strong>{{names}}</strong> are already using the <string>/{{slug}}</strong> url.",
|
||||
"managed_event_dialog_clarification": "If you choose to replace it, we will notify them. Go back and remove them if you don't want to overwrite it.",
|
||||
"review_event_type": "Review Event Type",
|
||||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"looking_for_more_insights": "Looking for more Insights?",
|
||||
"add_filter": "Add filter",
|
||||
"select_user": "Select User",
|
||||
@@ -1693,7 +1742,29 @@
|
||||
"redirect_url_warning": "Adding a redirect will disable the success page. Make sure to mention \"Booking Confirmed\" on your custom success page.",
|
||||
"event_trends": "Event Trends",
|
||||
"clear_filters": "Clear Filters",
|
||||
"hold": "Hold",
|
||||
"on_booking_option": "Collect payment on booking",
|
||||
"hold_option": "Charge no-show fee",
|
||||
"card_held": "Card held",
|
||||
"charge_card": "Charge card",
|
||||
"card_charged": "Card charged",
|
||||
"no_show_fee_amount": "{{amount, currency}} no-show fee",
|
||||
"no_show_fee": "No-show Fee",
|
||||
"submit_card": "Submit card",
|
||||
"submit_payment_information": "Submit payment information",
|
||||
"meeting_awaiting_payment_method": "Your meeting is awaiting a payment method",
|
||||
"no_show_fee_charged_email_subject": "No-show fee of {{amount, currency}} charged for {{title}} at {{date}}",
|
||||
"no_show_fee_charged_text_body": "No-show fee was charged",
|
||||
"no_show_fee_charged_subtitle": "No-show fee of {{amount, currency}} was charged for the following event",
|
||||
"error_charging_card": "Something went wrong charging the no-show fee. Please try again later.",
|
||||
"collect_no_show_fee": "Collect no-show fee",
|
||||
"no_show_fee_charged": "No-show fee charged",
|
||||
"insights": "Insights",
|
||||
"insights_no_data_found_for_filter": "No data found for the selected filter or selected dates.",
|
||||
"acknowledge_booking_no_show_fee": "I acknowledge that if I do not attend this event that a {{amount, currency}} no show fee will be applied to my card.",
|
||||
"card_details": "Card details",
|
||||
"seats_and_no_show_fee_error": "Currently cannot enable seats and charge a no-show fee",
|
||||
"complete_your_booking": "Complete your booking",
|
||||
"complete_your_booking_subject": "Complete your booking: {{title}} on {{date}}",
|
||||
"email_invite_team": "{{email}} has been invited"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user