Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
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 |
@@ -177,3 +177,5 @@ CSP_POLICY=
|
||||
|
||||
# Vercel Edge Config
|
||||
EDGE_CONFIG=
|
||||
|
||||
NEXT_PUBLIC_MINUTES_TO_BOOK=5 # Minutes
|
||||
@@ -2,6 +2,7 @@ name: PR Update
|
||||
|
||||
on:
|
||||
pull_request_target:
|
||||
types: [review_requested]
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
|
||||
@@ -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]"
|
||||
)}
|
||||
/>
|
||||
|
||||
@@ -2,7 +2,7 @@ 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];
|
||||
@@ -13,7 +13,7 @@ const BookingDescriptionPayment = (props: {
|
||||
|
||||
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]" />
|
||||
<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", {
|
||||
|
||||
@@ -28,16 +28,7 @@ import {
|
||||
TableActions,
|
||||
TextAreaField,
|
||||
} from "@calcom/ui";
|
||||
import {
|
||||
FiCheck,
|
||||
FiClock,
|
||||
FiMapPin,
|
||||
FiRefreshCcw,
|
||||
FiSend,
|
||||
FiSlash,
|
||||
FiX,
|
||||
FiCreditCard,
|
||||
} 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";
|
||||
|
||||
@@ -125,7 +116,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setRejectionDialogIsOpen(true);
|
||||
},
|
||||
icon: FiSlash,
|
||||
icon: Slash,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
// For bookings with payment, only confirm if the booking is paid for
|
||||
@@ -137,7 +128,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
bookingConfirm(true);
|
||||
},
|
||||
icon: FiCheck,
|
||||
icon: Check,
|
||||
disabled: mutation.isLoading,
|
||||
},
|
||||
]
|
||||
@@ -165,7 +156,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
isTabRecurring && isRecurring ? "&allRemainingBookings=true" : ""
|
||||
}${booking.seatsReferences.length ? `&seatReferenceUid=${getSeatReferenceUid()}` : ""}
|
||||
`,
|
||||
icon: FiX,
|
||||
icon: X,
|
||||
},
|
||||
{
|
||||
id: "edit_booking",
|
||||
@@ -173,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()}` : ""
|
||||
@@ -181,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: () => {
|
||||
@@ -194,7 +185,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setIsOpenLocationDialog(true);
|
||||
},
|
||||
icon: FiMapPin,
|
||||
icon: MapPin,
|
||||
},
|
||||
],
|
||||
},
|
||||
@@ -208,7 +199,7 @@ function BookingListItem(booking: BookingItemProps) {
|
||||
onClick: () => {
|
||||
setChargeCardDialogIsOpen(true);
|
||||
},
|
||||
icon: FiCreditCard,
|
||||
icon: CreditCard,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -222,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>
|
||||
);
|
||||
@@ -265,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
|
||||
@@ -518,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";
|
||||
|
||||
@@ -191,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">
|
||||
@@ -210,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]"
|
||||
@@ -221,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,
|
||||
@@ -246,7 +248,7 @@ 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]" />
|
||||
<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", {
|
||||
|
||||
@@ -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} 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">
|
||||
|
||||
@@ -14,7 +14,7 @@ import {
|
||||
DialogHeader,
|
||||
showToast,
|
||||
} from "@calcom/ui";
|
||||
import { FiCreditCard, FiAlertTriangle } from "@calcom/ui/components/icon";
|
||||
import { CreditCard, AlertTriangle } from "@calcom/ui/components/icon";
|
||||
|
||||
interface IRescheduleDialog {
|
||||
isOpenDialog: boolean;
|
||||
@@ -45,7 +45,7 @@ export const ChargeCardDialog = (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]">
|
||||
<FiCreditCard className="m-auto h-6 w-6" />
|
||||
<CreditCard className="m-auto h-6 w-6" />
|
||||
</div>
|
||||
<div className="pt-1">
|
||||
<DialogHeader title={t("charge_card")} />
|
||||
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
||||
|
||||
{chargeError && (
|
||||
<div className="mt-4 flex text-red-500">
|
||||
<FiAlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<AlertTriangle className="mr-2 h-5 w-5 " aria-hidden="true" />
|
||||
<p className="text-sm">{t("error_charging_card")}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
@@ -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,6 +10,7 @@ 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";
|
||||
@@ -17,7 +18,7 @@ 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, Alert } from "@calcom/ui";
|
||||
import { FiEdit, FiCopy } from "@calcom/ui/components/icon";
|
||||
import { Edit, Copy } from "@calcom/ui/components/icon";
|
||||
|
||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||
|
||||
@@ -80,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);
|
||||
@@ -128,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>
|
||||
@@ -150,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),
|
||||
@@ -158,6 +168,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
/>
|
||||
<hr className="border-subtle" />
|
||||
<RequiresConfirmationController
|
||||
eventType={eventType}
|
||||
seatsEnabled={seatsEnabled}
|
||||
metadata={eventType.metadata}
|
||||
requiresConfirmation={requiresConfirmation}
|
||||
@@ -171,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)}
|
||||
@@ -185,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) => {
|
||||
@@ -197,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"
|
||||
@@ -219,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) => {
|
||||
@@ -240,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) {
|
||||
@@ -247,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>
|
||||
}
|
||||
@@ -267,6 +284,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<SettingsToggle
|
||||
data-testid="offer-seats-toggle"
|
||||
title={t("offer_seats")}
|
||||
{...seatsLocked}
|
||||
description={t("offer_seats_description")}
|
||||
checked={value}
|
||||
disabled={noShowFeeEnabled}
|
||||
@@ -295,8 +313,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
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)));
|
||||
@@ -305,6 +325,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
||||
<div className="mt-2">
|
||||
<Checkbox
|
||||
description={t("show_attendees")}
|
||||
disabled={seatsLocked.disabled}
|
||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
||||
defaultChecked={!!eventType.seatsShowAttendees}
|
||||
/>
|
||||
|
||||
@@ -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.14",
|
||||
"version": "2.7.17",
|
||||
"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>
|
||||
|
||||
@@ -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";
|
||||
@@ -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);
|
||||
@@ -126,6 +132,41 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
let message = "";
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
message = `${err.data.code}: You are not able to update this event`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
|
||||
message = `${err.data.code}: ${err.message}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
showToast(message, "error");
|
||||
} else {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
||||
startDate: new Date(eventType.periodStartDate || Date.now()),
|
||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
||||
@@ -173,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>({
|
||||
@@ -200,48 +253,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
||||
),
|
||||
});
|
||||
|
||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||
onSuccess: async () => {
|
||||
showToast(
|
||||
t("event_type_updated_successfully", {
|
||||
eventTypeTitle: eventType.title,
|
||||
}),
|
||||
"success"
|
||||
);
|
||||
},
|
||||
async onSettled() {
|
||||
await utils.viewer.eventTypes.get.invalidate();
|
||||
},
|
||||
onError: (err) => {
|
||||
let message = "";
|
||||
if (err instanceof HttpError) {
|
||||
const message = `${err.statusCode}: ${err.message}`;
|
||||
showToast(message, "error");
|
||||
}
|
||||
|
||||
if (err.data?.code === "UNAUTHORIZED") {
|
||||
message = `${err.data.code}: You are not able to update this event`;
|
||||
}
|
||||
|
||||
if (err.data?.code === "PARSE_ERROR" || err.data?.code === "BAD_REQUEST") {
|
||||
message = `${err.data.code}: ${err.message}`;
|
||||
}
|
||||
|
||||
if (message) {
|
||||
showToast(message, "error");
|
||||
} else {
|
||||
showToast(err.message, "error");
|
||||
}
|
||||
|
||||
console.log(formMethods.formState.isSubmitting);
|
||||
|
||||
// formMethods.setFormState((prevState) => ({
|
||||
// ...prevState,
|
||||
// isSubmitting: false,
|
||||
// }));
|
||||
},
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
if (!formMethods.formState.isDirty) {
|
||||
//TODO: What's the best way to sync the form with backend
|
||||
@@ -273,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} />,
|
||||
@@ -288,89 +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"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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,
|
||||
});
|
||||
<>
|
||||
<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"),
|
||||
},
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -5,7 +5,7 @@ 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();
|
||||
@@ -16,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")}
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
@@ -69,6 +69,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 +226,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 +589,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 +754,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 +1201,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 +1280,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 +1470,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 +1695,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",
|
||||
|
||||
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "Aviso de Reserva",
|
||||
"slot_interval": "Intervalos de tiempo",
|
||||
"slot_interval_default": "Usar duración del evento (por defecto)",
|
||||
"delete_event_type_description": "¿Está seguro de que desea eliminar este tipo de evento? Cualquiera con quien hayas compartido este enlace ya no podrá reservar con él.",
|
||||
"delete_event_type": "Eliminar Tipo de Evento",
|
||||
"confirm_delete_event_type": "Sí, Eliminar Tipo de Evento",
|
||||
"delete_account": "Eliminar cuenta",
|
||||
"confirm_delete_account": "Sí, eliminar cuenta",
|
||||
"delete_account_confirmation_message": "¿Estás seguro de que quieres eliminar tu cuenta de {{appName}}? Cualquier persona con la que hayas compartido el enlace de tu cuenta ya no podrá agendar con ella y cualquier preferencia que hayas guardado se perderá.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "Código de verificación",
|
||||
"can_you_try_again": "¿Puede intentarlo de nuevo con una hora diferente?",
|
||||
"verify": "Verificar",
|
||||
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
|
||||
"select_all": "Seleccionar todo",
|
||||
"default_conferencing_bulk_title": "Actualización masiva de tipos de eventos existentes",
|
||||
"invalid_event_name_variables": "Hay una variable no válida en el nombre de su evento",
|
||||
"default_conferencing_bulk_description": "Actualizar las ubicaciones para los tipos de eventos seleccionados"
|
||||
}
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
"calcom_explained_new_user": "Terminez la configuration de votre compte {{appName}} ! Vous n'êtes qu'à quelques pas de résoudre tous vos problèmes de planification.",
|
||||
"have_any_questions": "Vous avez des questions ? Nous sommes là pour vous aider.",
|
||||
"reset_password_subject": "{{appName}} : Instructions de réinitialisation de mot de passe",
|
||||
"event_declined_subject": "Refusé : {{title}} à {{date}}",
|
||||
"event_cancelled_subject": "Annulé : {{title}} à {{date}}",
|
||||
"event_declined_subject": "Refusé : {{title}} le {{date}}",
|
||||
"event_cancelled_subject": "Annulé : {{title}} le {{date}}",
|
||||
"event_request_declined": "Votre demande d'événement a été refusée",
|
||||
"event_request_declined_recurring": "Votre demande d'événement récurrent a été refusée",
|
||||
"event_request_cancelled": "Votre événement planifié a été annulé",
|
||||
@@ -66,11 +66,13 @@
|
||||
"someone_requested_an_event": "Quelqu'un a demandé à planifier un événement sur votre calendrier.",
|
||||
"someone_requested_password_reset": "Quelqu'un a demandé un lien pour modifier votre mot de passe.",
|
||||
"password_reset_instructions": "Si vous n'en avez pas fait la demande, vous pouvez ignorer cet e-mail en toute sécurité et votre mot de passe ne sera pas modifié.",
|
||||
"event_awaiting_approval_subject": "En attente de validation : {{title}} à {{date}}",
|
||||
"event_awaiting_approval_subject": "En attente de validation : {{title}} le {{date}}",
|
||||
"event_still_awaiting_approval": "Un événement est toujours en attente de votre validation",
|
||||
"booking_submitted_subject": "Réservation confirmée : {{eventType}} avec {{name}} le {{date}}",
|
||||
"booking_submitted_subject": "Réservation confirmée : {{title}} le {{date}}",
|
||||
"download_recording_subject": "Télécharger l'enregistrement : {{title}} le {{date}}",
|
||||
"download_your_recording": "Télécharger votre enregistrement",
|
||||
"your_meeting_has_been_booked": "Votre rendez-vous a été réservé",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié à {{date}}",
|
||||
"event_type_has_been_rescheduled_on_time_date": "Votre {{title}} a été replanifié le {{date}}",
|
||||
"event_has_been_rescheduled": "Mise à jour - Votre événement a été replanifié",
|
||||
"request_reschedule_title_attendee": "Demander à replanifier votre réservation",
|
||||
"request_reschedule_subtitle": "{{organizer}} a annulé la réservation et vous a demandé de choisir un autre moment.",
|
||||
@@ -224,6 +226,7 @@
|
||||
"done": "Terminé",
|
||||
"all_done": "Terminé !",
|
||||
"all_apps": "Toutes",
|
||||
"available_apps": "Applications disponibles",
|
||||
"check_email_reset_password": "Vérifiez vos e-mails. Nous vous avons envoyé un lien pour réinitialiser votre mot de passe.",
|
||||
"finish": "Terminer",
|
||||
"few_sentences_about_yourself": "Quelques mots à propos de vous. Ces informations apparaîtront sur votre page publique.",
|
||||
@@ -394,7 +397,7 @@
|
||||
"our_support_team": "notre équipe d'assistance",
|
||||
"contact_our_support_team": "Contactez notre équipe d'assistance",
|
||||
"uh_oh": "Oups !",
|
||||
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de types d'événement.",
|
||||
"no_event_types_have_been_setup": "Cet utilisateur n'a pas encore configuré de type d'événement.",
|
||||
"edit_logo": "Modifier le logo",
|
||||
"upload_a_logo": "Télécharger un logo",
|
||||
"remove_logo": "Supprimer le logo",
|
||||
@@ -586,6 +589,20 @@
|
||||
"minutes": "minutes",
|
||||
"round_robin": "Round-Robin",
|
||||
"round_robin_description": "Alternez vos rendez-vous entre plusieurs membres d'équipe.",
|
||||
"managed_event": "Événement géré",
|
||||
"username_placeholder": "nom d'utilisateur",
|
||||
"managed_event_description": "Créez et distribuez plusieurs types d'événements aux membres de l'équipe.",
|
||||
"managed": "Géré",
|
||||
"managed_event_url_clarification": "« nom d'utilisateur » sera rempli par le nom d'utilisateur des membres assignés",
|
||||
"assign_to": "Assigner à",
|
||||
"add_members": "Ajouter des membres...",
|
||||
"count_members_one": "{{count}} membre",
|
||||
"count_members_other": "{{count}} membres",
|
||||
"no_assigned_members": "Aucun membre assigné",
|
||||
"assigned_to": "Assigné à",
|
||||
"start_assigning_members_above": "Commencez à assigner des membres ci-dessus",
|
||||
"locked_fields_admin_description": "Les membres ne pourront pas modifier ceci.",
|
||||
"locked_fields_member_description": "Cette option a été verrouillée par l'administrateur de l'équipe.",
|
||||
"url": "Lien",
|
||||
"hidden": "Masqué",
|
||||
"readonly": "Lecture seule",
|
||||
@@ -595,7 +612,7 @@
|
||||
"plan_upgrade": "Vous devez mettre à niveau votre plan pour avoir plus d'un type d'événement actif.",
|
||||
"plan_upgrade_teams": "Vous devez mettre à niveau votre plan pour créer une équipe.",
|
||||
"plan_upgrade_instructions": "Vous pouvez <1>mettre à niveau ici</1>.",
|
||||
"event_types_page_title": "Types d’événement",
|
||||
"event_types_page_title": "Types d'évènements",
|
||||
"event_types_page_subtitle": "Créez des événements à partager pour que les personnes puissent réserver des créneaux sur votre calendrier.",
|
||||
"new": "Nouveau",
|
||||
"new_event_type_btn": "Nouveau type d'événement",
|
||||
@@ -737,9 +754,11 @@
|
||||
"minimum_booking_notice": "Préavis minimum",
|
||||
"slot_interval": "Fréquence de créneaux horaires",
|
||||
"slot_interval_default": "Utiliser la durée de l'événement (par défaut)",
|
||||
"delete_event_type_description": "Voulez-vous vraiment supprimer ce type d'événement ? Toute personne avec qui vous avez partagé ce lien ne pourra plus faire de réservation en l'utilisant.",
|
||||
"delete_event_type": "Supprimer le type d'événement",
|
||||
"confirm_delete_event_type": "Oui, supprimer le type d'événement",
|
||||
"delete_event_type": "Supprimer le type d'événement ?",
|
||||
"delete_managed_event_type": "Supprimer le type d'événement géré ?",
|
||||
"delete_event_type_description": "Toute personne avec qui vous avez partagé ce lien ne pourra plus réserver en l'utilisant.",
|
||||
"delete_managed_event_type_description": "<ul><li>Les membres affectés à ce type d'événement verront également leurs types d'événement supprimés.</li><li>Toute personne avec qui ils ont partagé leur lien ne pourra plus réserver en l'utilisant.</li></ul>",
|
||||
"confirm_delete_event_type": "Oui, supprimer",
|
||||
"delete_account": "Supprimer le compte",
|
||||
"confirm_delete_account": "Oui, supprimer le compte",
|
||||
"delete_account_confirmation_message": "Voulez-vous vraiment supprimer votre compte {{appName}} ? Toute personne avec qui vous avez partagé le lien de votre compte ne pourra plus réserver en utilisant ce lien et toutes les préférences que vous avez sauvegardées seront perdues.",
|
||||
@@ -944,7 +963,7 @@
|
||||
"impersonating_user_warning": "Emprunt du nom d'utilisateur « {{user}} ».",
|
||||
"impersonating_stop_instructions": "Cliquez ici pour arrêter",
|
||||
"event_location_changed": "Mise à jour - Votre événement a changé de lieu",
|
||||
"location_changed_event_type_subject": "Lieu modifié : {{eventType}} avec {{name}} à {{date}}",
|
||||
"location_changed_event_type_subject": "Lieu modifié : {{eventType}} avec {{name}} le {{date}}",
|
||||
"current_location": "Lieu actuel",
|
||||
"user_phone": "Votre numéro de téléphone",
|
||||
"new_location": "Nouveau lieu",
|
||||
@@ -1120,8 +1139,8 @@
|
||||
"close": "Fermer",
|
||||
"upgrade": "Mettre à niveau",
|
||||
"upgrade_to_access_recordings_title": "Mettez à niveau pour accéder aux enregistrements",
|
||||
"upgrade_to_access_recordings_description": "Les enregistrements ne sont disponibles que dans le plan d'équipes. Mettez à niveau pour commencer à enregistrer vos appels.",
|
||||
"recordings_are_part_of_the_teams_plan": "Les enregistrements font partie du plan d'équipes",
|
||||
"upgrade_to_access_recordings_description": "Les enregistrements ne sont disponibles que dans le plan Équipes. Mettez à niveau pour commencer à enregistrer vos appels.",
|
||||
"recordings_are_part_of_the_teams_plan": "Les enregistrements font partie du plan Équipes",
|
||||
"team_feature_teams": "Ceci est une fonctionnalité d'équipe. Mettez à jour vers le plan Équipes pour voir les disponibilités de votre équipe.",
|
||||
"team_feature_workflows": "Ceci est une fonctionnalité d'équipe. Mettez à jour vers le plan Équipes pour automatiser vos notifications et rappels d'événements avec les workflows.",
|
||||
"show_eventtype_on_profile": "Afficher sur le profil",
|
||||
@@ -1182,6 +1201,7 @@
|
||||
"start_of_week": "Début de la semaine",
|
||||
"recordings_title": "Enregistrements",
|
||||
"recording": "Enregistrement",
|
||||
"happy_scheduling": "Bonne planification",
|
||||
"select_calendars": "Sélectionnez les calendriers dont vous souhaitez vérifier les conflits afin d'éviter les réservations en double.",
|
||||
"check_for_conflicts": "Vérifier les conflits",
|
||||
"view_recordings": "Voir les enregistrements",
|
||||
@@ -1260,6 +1280,8 @@
|
||||
"download_responses": "Télécharger les réponses",
|
||||
"download_responses_description": "Téléchargez toutes les réponses à votre formulaire au format CSV.",
|
||||
"download": "Télécharger",
|
||||
"download_recording": "Télécharger l'enregistrement",
|
||||
"recording_from_your_recent_call": "Un enregistrement de votre appel récent sur Cal.com est prêt à être téléchargé",
|
||||
"create_your_first_form": "Créez votre premier formulaire",
|
||||
"create_your_first_form_description": "Avec les formulaires de routage, vous pouvez poser des questions de qualification et rediriger vers la bonne personne ou le bon type d'événement.",
|
||||
"create_your_first_webhook": "Créez votre premier webhook",
|
||||
@@ -1448,11 +1470,17 @@
|
||||
"fixed_round_robin": "Round-Robin fixe",
|
||||
"add_one_fixed_attendee": "Ajoutez un participant fixe et le Round-Robin via un certain nombre de participants.",
|
||||
"calcom_is_better_with_team": "Cal.com est meilleur en équipe",
|
||||
"the_calcom_team": "L'équipe Cal.com",
|
||||
"add_your_team_members": "Ajoutez les membres de votre équipe à vos types d'événements. Utilisez la planification collective pour inclure tout le monde ou trouvez la personne la plus appropriée avec la planification Round-Robin.",
|
||||
"booking_limit_reached": "La limite de réservation pour ce type d'événement a été atteinte",
|
||||
"duration_limit_reached": "La limite de durée pour ce type d'événement a été atteinte",
|
||||
"admin_has_disabled": "Un administrateur a désactivé {{appName}}",
|
||||
"disabled_app_affects_event_type": "Un administrateur a désactivé {{appName}} qui affecte votre type d'événement {{eventType}}",
|
||||
"event_replaced_notice": "Un administrateur a remplacé l'un de vos types d'événements",
|
||||
"email_subject_slug_replacement": "Un administrateur d'équipe a remplacé votre événement /{{slug}}",
|
||||
"email_body_slug_replacement_notice": "Un administrateur de l'équipe <strong>{{teamName}}</strong> a remplacé votre type d'événement <strong>/{{slug}}</strong> par un type d'événement géré qu'il contrôle.",
|
||||
"email_body_slug_replacement_info": "Votre lien continuera à fonctionner, mais certains de ses paramètres peuvent avoir changé. Vous pouvez l'examiner dans les types d'événements.",
|
||||
"email_body_slug_replacement_suggestion": "Si vous avez des questions sur le type d'événement, veuillez contacter votre administrateur.<br /><br />Bonne planification, <br />L'équipe Cal.com",
|
||||
"disable_payment_app": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}. Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre moyen de paiement.",
|
||||
"payment_disabled_still_able_to_book": "Les participants sont toujours en mesure de réserver ce type d'événement, mais ne seront pas invités à payer. Vous pouvez masquer le type d'événement pour l'empêcher jusqu'à ce que votre administrateur réactive votre mode de paiement.",
|
||||
"app_disabled_with_event_type": "L'administrateur a désactivé {{appName}} qui affecte votre type d'événement {{title}}.",
|
||||
@@ -1649,13 +1677,13 @@
|
||||
"add_1_option_per_line": "Ajouter une option par ligne",
|
||||
"select_a_router": "Sélectionner un routeur",
|
||||
"add_a_new_route": "Ajouter une nouvelle route",
|
||||
"make_informed_decisions": "Prenez des décisions éclairées avec Insights",
|
||||
"make_informed_decisions_description": "Notre tableau de bord Insights analyse toute l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
|
||||
"view_bookings_across": "Voir les réservations de tous les membres",
|
||||
"make_informed_decisions": "Prenez des décisions éclairées grâce aux statistiques",
|
||||
"make_informed_decisions_description": "Notre tableau de bord de statistiques analyse l'activité de votre équipe et vous révèle les tendances qui permettent d'améliorer la planification et la prise de décision au sein de l'équipe.",
|
||||
"view_bookings_across": "Visualisez les réservations de tous les membres",
|
||||
"view_bookings_across_description": "Découvrez qui reçoit le plus de réservations et assurez la meilleure répartition au sein de votre équipe.",
|
||||
"identify_booking_trends": "Identifier les tendances de réservation",
|
||||
"identify_booking_trends": "Identifiez les tendances de réservation",
|
||||
"identify_booking_trends_description": "Découvrez quels jours de la semaine et quelles heures de la journée sont populaires auprès de vos utilisateurs.",
|
||||
"spot_popular_event_types": "Identifier les types d'événements populaires",
|
||||
"spot_popular_event_types": "Identifiez les types d'événements populaires",
|
||||
"spot_popular_event_types_description": "Découvrez lesquels de vos types d'événements reçoivent le plus de clics et de réservations.",
|
||||
"no_responses_yet": "Aucune réponse pour l'instant",
|
||||
"this_will_be_the_placeholder": "Ce sera le texte de substitution",
|
||||
@@ -1667,11 +1695,31 @@
|
||||
"verification_code": "Code de vérification",
|
||||
"can_you_try_again": "Pouvez-vous réessayer avec un autre créneau ?",
|
||||
"verify": "Vérifier",
|
||||
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
|
||||
"select_all": "Tout sélectionner",
|
||||
"default_conferencing_bulk_title": "Mise à jour multiple des types d'événements existants",
|
||||
"invalid_event_name_variables": "Il y a une variable invalide dans le nom de votre événement",
|
||||
"members_default_schedule": "Planning par défaut du membre",
|
||||
"set_by_admin": "Défini par l'administrateur de l'équipe",
|
||||
"members_default_location": "Lieu par défaut du membre",
|
||||
"members_default_schedule_description": "Nous utiliserons le calendrier de disponibilité par défaut de chaque membre. Ils pourront le modifier ou le changer.",
|
||||
"requires_at_least_one_schedule": "Vous devez avoir au moins un planning",
|
||||
"default_conferencing_bulk_description": "Mettez à jour les emplacements pour les types d'événements sélectionnés.",
|
||||
"locked_for_members": "Verrouillé pour les membres",
|
||||
"locked_apps_description": "Les membres pourront voir les applications actives, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_webhooks_description": "Les membres pourront voir les webhooks actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_workflows_description": "Les membres pourront voir les workflows actifs, mais ne pourront pas modifier leurs paramètres.",
|
||||
"locked_by_admin": "Verrouillé par l'administrateur",
|
||||
"app_not_connected": "Vous n'avez pas connecté de compte {{appName}}.",
|
||||
"connect_now": "Connecter maintenant",
|
||||
"managed_event_dialog_confirm_button_one": "Remplacer et notifier {{count}} membre",
|
||||
"managed_event_dialog_confirm_button_other": "Remplacer et notifier {{count}} membres",
|
||||
"managed_event_dialog_title_one": "Le lien /{{slug}} existe déjà pour {{count}} membre. Voulez-vous le remplacer ?",
|
||||
"managed_event_dialog_title_other": "Le lien /{{slug}} existe déjà pour {{count}} membres. Voulez-vous le remplacer ?",
|
||||
"managed_event_dialog_information_one": "<strong>{{names}}</strong> utilise déjà le lien <strong>/{{slug}}</strong>.",
|
||||
"managed_event_dialog_information_other": "<strong>{{names}}</strong> utilisent déjà le lien <strong>/{{slug}}</strong>.",
|
||||
"managed_event_dialog_clarification": "Si vous choisissez de le remplacer, nous les en informerons. Revenez en arrière et supprimez-les si vous ne souhaitez pas l'écraser.",
|
||||
"review_event_type": "Examiner le type d'événement",
|
||||
"looking_for_more_analytics": "Vous cherchez plus d'analyses ?",
|
||||
"looking_for_more_insights": "Vous cherchez plus de statistiques ?",
|
||||
"add_filter": "Ajouter un filtre",
|
||||
"select_user": "Sélectionner un utilisateur",
|
||||
@@ -1688,12 +1736,34 @@
|
||||
"events_rescheduled": "Événements replanifiés",
|
||||
"from_last_period": "depuis la dernière période",
|
||||
"from_to_date_period": "De : {{startDate}} À : {{endDate}}",
|
||||
"analytics_for_organisation": "Insights",
|
||||
"analytics_for_organisation": "Statistiques",
|
||||
"subtitle_analytics": "En savoir plus sur l'activité de votre équipe",
|
||||
"redirect_url_warning": "L'ajout d'une redirection désactivera la page de succès. Assurez-vous de mentionner « Réservation confirmée » sur votre page de succès personnalisée.",
|
||||
"event_trends": "Tendances de l'événement",
|
||||
"clear_filters": "Effacer les filtres",
|
||||
"insights": "Insights",
|
||||
"hold": "Retenir",
|
||||
"on_booking_option": "Percevoir le paiement à la réservation",
|
||||
"hold_option": "Facturer des frais d'absence",
|
||||
"card_held": "Carte retenue",
|
||||
"charge_card": "Débiter la carte",
|
||||
"card_charged": "Carte débitée",
|
||||
"no_show_fee_amount": "Frais d'absence de {{amount, currency}}",
|
||||
"no_show_fee": "Frais d'absence",
|
||||
"submit_card": "Soumettre la carte",
|
||||
"submit_payment_information": "Soumettre les informations de paiement",
|
||||
"meeting_awaiting_payment_method": "Votre rendez-vous est en attente d'un moyen de paiement",
|
||||
"no_show_fee_charged_email_subject": "Frais d'absence de {{amount, currency}} facturés pour {{title}} le {{date}}",
|
||||
"no_show_fee_charged_text_body": "Des frais d'absence ont été facturés",
|
||||
"no_show_fee_charged_subtitle": "Des frais d'absence de {{amount, currency}} ont été facturés pour l'événement suivant",
|
||||
"error_charging_card": "Une erreur s'est produite lors de la facturation des frais d'absence. Veuillez réessayer plus tard.",
|
||||
"collect_no_show_fee": "Percevoir les frais d'absence",
|
||||
"no_show_fee_charged": "Frais d'absence facturés",
|
||||
"insights": "Statistiques",
|
||||
"insights_no_data_found_for_filter": "Aucune donnée trouvée pour le filtre ou les dates sélectionnés.",
|
||||
"acknowledge_booking_no_show_fee": "Je reconnais que si je ne participe pas à cet événement, des frais d'absence de {{amount, currency}} seront appliqués à ma carte.",
|
||||
"card_details": "Détails de la carte",
|
||||
"seats_and_no_show_fee_error": "Il n'est pas possible d'activer les places et de facturer des frais d'absence pour le moment",
|
||||
"complete_your_booking": "Terminer votre réservation",
|
||||
"complete_your_booking_subject": "Terminer votre réservation : {{title}} le {{date}}",
|
||||
"email_invite_team": "{{email}} a été invité"
|
||||
}
|
||||
|
||||
@@ -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}} שלך? כל מי ששיתפת איתו את הקישור לחשבון לא יוכל יותר להזמין באמצעותו, וכל ההעדפות ששמרת יאבדו.",
|
||||
@@ -1660,11 +1657,12 @@
|
||||
"verification_code": "קוד אימות",
|
||||
"can_you_try_again": "אתה יכול לנסות שוב עם שעה אחרת?",
|
||||
"verify": "אמת",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
|
||||
"select_all": "בחר הכל",
|
||||
"default_conferencing_bulk_title": "בצע עדכון אצווה של סוגי הארועים הקיימים",
|
||||
"invalid_event_name_variables": "יש משתנה שגוי בשם הארוע שלך",
|
||||
"requires_at_least_one_schedule": "אתה חייב שיהיה לך לפחות לוח זמנים אחד",
|
||||
"default_conferencing_bulk_description": "עדכן את המיקומים עבור סוגי הארועים שנבחרו",
|
||||
"looking_for_more_analytics": "מחפש עוד מידע אנליטי?",
|
||||
"add_filter": "הוסף סנן",
|
||||
"select_user": "בחר משתמש",
|
||||
"select_event_type": "בחר סוג ארוע",
|
||||
|
||||
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "Preavviso minimo",
|
||||
"slot_interval": "Intervalli di fascia oraria",
|
||||
"slot_interval_default": "Usa durata evento (impostazione predefinita)",
|
||||
"delete_event_type_description": "Sei sicuro di voler eliminare questo tipo di evento? Chiunque abbia condiviso questo link non potrà più utilizzarlo per prenotare.",
|
||||
"delete_event_type": "Elimina Tipo Evento",
|
||||
"confirm_delete_event_type": "Sì, elimina tipo evento",
|
||||
"delete_account": "Elimina account",
|
||||
"confirm_delete_account": "Sì, elimina account",
|
||||
"delete_account_confirmation_message": "Sei sicuro di voler eliminare il tuo account {{appName}}? Chiunque abbia il link del tuo account non potrà più utilizzarlo per prenotare e le preferenze che hai salvato andranno perse.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "Codice di verifica",
|
||||
"can_you_try_again": "Riprovare specificando un orario differente.",
|
||||
"verify": "Verifica",
|
||||
"invalid_event_name_variables": "Nel nome dell'evento è presente una variabile non valida",
|
||||
"select_all": "Seleziona tutto",
|
||||
"default_conferencing_bulk_title": "Aggiorna in blocco i tipi di eventi esistenti",
|
||||
"invalid_event_name_variables": "Nel nome dell'evento è presente una variabile non valida",
|
||||
"default_conferencing_bulk_description": "Aggiorna le posizioni per i tipi di eventi selezionati"
|
||||
}
|
||||
|
||||
@@ -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": "別のタイミングでもう1度お試しください",
|
||||
"verify": "確認する",
|
||||
"invalid_event_name_variables": "イベント名に無効な変数があります",
|
||||
"select_all": "すべてを選択",
|
||||
"default_conferencing_bulk_title": "既存のイベントタイプを一括更新する",
|
||||
"invalid_event_name_variables": "イベント名に無効な変数があります",
|
||||
"default_conferencing_bulk_description": "選択したイベントタイプの場所を更新します"
|
||||
}
|
||||
|
||||
@@ -737,9 +737,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}} 계정을 삭제하시겠습니까? 계정 링크를 공유한 사람은 더 이상 이 링크를 사용하여 예약할 수 없으며 저장한 모든 기본 설정이 손실됩니다.",
|
||||
@@ -1663,9 +1660,9 @@
|
||||
"verification_code": "인증 코드",
|
||||
"can_you_try_again": "다른 시간으로 다시 해보시겠어요?",
|
||||
"verify": "인증",
|
||||
"invalid_event_name_variables": "이벤트 이름에 유효하지 않은 변수가 있습니다",
|
||||
"select_all": "모두 선택",
|
||||
"default_conferencing_bulk_title": "기존 이벤트 유형 일괄 업데이트",
|
||||
"invalid_event_name_variables": "이벤트 이름에 유효하지 않은 변수가 있습니다",
|
||||
"requires_at_least_one_schedule": "적어도 한 개 이상의 스케줄이 필요합니다",
|
||||
"default_conferencing_bulk_description": "선택한 이벤트 유형의 위치 업데이트",
|
||||
"looking_for_more_insights": "더 많은 인사이트를 찾고계신가요?",
|
||||
|
||||
@@ -736,9 +736,6 @@
|
||||
"minimum_booking_notice": "Minimale kennisgevingstermijn",
|
||||
"slot_interval": "Tijdslot intervallen",
|
||||
"slot_interval_default": "Gebruik evenement lengte (standaard)",
|
||||
"delete_event_type_description": "Weet u zeker dat u dit evenement wilt verwijderen? Iedereen met wie u deze link heeft gedeeld zal hem niet meer kunnen gebruiken om te boeken.",
|
||||
"delete_event_type": "Verwijder Evenement",
|
||||
"confirm_delete_event_type": "Ja, verwijder evenement",
|
||||
"delete_account": "Account verwijderen",
|
||||
"confirm_delete_account": "Ja, account verwijderen",
|
||||
"delete_account_confirmation_message": "Weet u zeker dat u uw {{appName}}-account wilt verwijderen? Iedereen met wie u uw accountlink gedeeld heeft, kan er niet meer mee boeken en alle voorkeuren die u opgeslagen heeft gaan verloren.",
|
||||
@@ -1652,8 +1649,8 @@
|
||||
"verification_code": "Verificatiecode",
|
||||
"can_you_try_again": "Kunt u het opnieuw proberen met een andere tijd?",
|
||||
"verify": "Verifiëren",
|
||||
"invalid_event_name_variables": "Uw gebeurtenisnaam bevat een ongeldige variabele",
|
||||
"select_all": "Alles selecteren",
|
||||
"default_conferencing_bulk_title": "Bestaande gebeurtenistypen in bulk bijwerken",
|
||||
"invalid_event_name_variables": "Uw gebeurtenisnaam bevat een ongeldige variabele",
|
||||
"default_conferencing_bulk_description": "Werk de locaties voor de geselecteerde gebeurtenistypen bij"
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user