Merged with main
This commit is contained in:
commit
302e62b020
2
apps/api
2
apps/api
|
@ -1 +1 @@
|
|||
Subproject commit a36b6fa923def51e837caf1229d1eac9b582a502
|
||||
Subproject commit 7e9226fabcea3b86303df5bd18643f96f63fc385
|
|
@ -44,6 +44,11 @@ function BookingListItem(booking: BookingItemProps) {
|
|||
const mutation = trpc.useMutation(["viewer.bookings.confirm"], {
|
||||
onSuccess: () => {
|
||||
setRejectionDialogIsOpen(false);
|
||||
showToast(t("booking_confirmation_success"), "success");
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
},
|
||||
onError: () => {
|
||||
showToast(t("booking_confirmation_failed"), "error");
|
||||
utils.invalidateQueries("viewer.bookings");
|
||||
},
|
||||
});
|
||||
|
|
|
@ -33,7 +33,7 @@ const TimeOptions: FC<Props> = ({ onToggle24hClock, onSelectTimeZone, timeFormat
|
|||
};
|
||||
|
||||
return selectedTimeZone !== "" ? (
|
||||
<div className="max-w-80 dark:border-darkgray-300 dark:bg-darkgray-200 absolute z-10 w-full rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
||||
<div className="dark:border-darkgray-300 dark:bg-darkgray-200 rounded-sm border border-gray-200 bg-white px-4 pt-4 pb-3 shadow-sm">
|
||||
<div className="mb-4 flex">
|
||||
<div className="text-sm font-medium text-gray-600 dark:text-white">{t("time_options")}</div>
|
||||
<div className="ml-auto flex items-center">
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
// Get router variables
|
||||
import { EventType } from "@prisma/client";
|
||||
import { SchedulingType } from "@prisma/client";
|
||||
import * as Collapsible from "@radix-ui/react-collapsible";
|
||||
import * as Popover from "@radix-ui/react-popover";
|
||||
import { TFunction } from "next-i18next";
|
||||
import { useRouter } from "next/router";
|
||||
import { useReducer, useEffect, useMemo, useState } from "react";
|
||||
|
@ -240,8 +240,8 @@ function TimezoneDropdown({
|
|||
};
|
||||
|
||||
return (
|
||||
<Collapsible.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen} className="flex">
|
||||
<Collapsible.Trigger className="min-w-32 dark:text-darkgray-600 mb-2 -ml-2 px-2 text-left text-gray-600">
|
||||
<Popover.Root open={isTimeOptionsOpen} onOpenChange={setIsTimeOptionsOpen}>
|
||||
<Popover.Trigger className="min-w-32 dark:text-darkgray-600 radix-state-open:bg-gray-200 dark:radix-state-open:bg-darkgray-200 group relative mb-2 -ml-2 inline-block rounded-md px-2 py-2 text-left text-gray-600">
|
||||
<p className="text-sm font-medium">
|
||||
<Icon.FiGlobe className="mr-[10px] ml-[2px] -mt-[2px] inline-block h-4 w-4" />
|
||||
{timeZone}
|
||||
|
@ -251,15 +251,20 @@ function TimezoneDropdown({
|
|||
<Icon.FiChevronDown className="ml-1 inline-block h-4 w-4" />
|
||||
)}
|
||||
</p>
|
||||
</Collapsible.Trigger>
|
||||
<Collapsible.Content>
|
||||
<TimeOptions
|
||||
onSelectTimeZone={handleSelectTimeZone}
|
||||
onToggle24hClock={handleToggle24hClock}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
</Collapsible.Content>
|
||||
</Collapsible.Root>
|
||||
</Popover.Trigger>
|
||||
<Popover.Portal>
|
||||
<Popover.Content
|
||||
hideWhenDetached
|
||||
align="start"
|
||||
className="animate-fade-in-up absolute left-0 top-2 w-80 max-w-[calc(100vw_-_1.5rem)]">
|
||||
<TimeOptions
|
||||
onSelectTimeZone={handleSelectTimeZone}
|
||||
onToggle24hClock={handleToggle24hClock}
|
||||
timeFormat={timeFormat}
|
||||
/>
|
||||
</Popover.Content>
|
||||
</Popover.Portal>
|
||||
</Popover.Root>
|
||||
);
|
||||
}
|
||||
|
||||
|
@ -416,7 +421,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||
<h1 className="text-bookingdark dark:text-darkgray-900 mb-4 break-words text-xl font-semibold">
|
||||
{eventType.title}
|
||||
</h1>
|
||||
<div className="flex flex-col space-y-3">
|
||||
<div className="flex flex-col items-start space-y-3">
|
||||
{eventType?.description && (
|
||||
<div className="dark:text-darkgray-600 flex py-1 text-sm font-medium text-gray-600">
|
||||
<div>
|
||||
|
@ -500,7 +505,7 @@ const AvailabilityPage = ({ profile, eventType }: Props) => {
|
|||
<div className="overflow-hidden sm:flex">
|
||||
<div
|
||||
className={
|
||||
"sm:dark:border-darkgray-200 hidden overflow-hidden border-gray-200 p-5 sm:border-r md:flex md:flex-col " +
|
||||
"sm:dark:border-darkgray-200 hidden border-gray-200 p-5 sm:border-r md:flex md:flex-col " +
|
||||
(isAvailableTimesVisible ? "sm:w-1/3" : recurringEventCount ? "sm:w-2/3" : "sm:w-1/2")
|
||||
}>
|
||||
<UserAvatars
|
||||
|
|
|
@ -14,10 +14,11 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||
const { title, imageSrc, type } = props;
|
||||
const { t } = useLocale();
|
||||
return (
|
||||
<div className="flex flex-row items-center p-5">
|
||||
<img src={imageSrc} alt={title} className="h-8 w-8" />
|
||||
<p className="mx-3 text-sm font-bold">{title}</p>
|
||||
|
||||
<div className="flex flex-row items-center justify-between p-5">
|
||||
<div className="flex items-center space-x-3">
|
||||
<img src={imageSrc} alt={title} className="h-8 w-8" />
|
||||
<p className="text-sm font-bold">{title}</p>
|
||||
</div>
|
||||
<InstallAppButtonWithoutPlanCheck
|
||||
type={type}
|
||||
render={(buttonProps) => (
|
||||
|
@ -29,8 +30,7 @@ const CalendarItem = (props: ICalendarItem) => {
|
|||
// Save cookie key to return url step
|
||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||
buttonProps && buttonProps.onClick && buttonProps?.onClick(event);
|
||||
}}
|
||||
className="ml-auto rounded-md border border-gray-200 py-[10px] px-4 text-sm font-bold">
|
||||
}}>
|
||||
{t("connect")}
|
||||
</Button>
|
||||
)}
|
||||
|
|
|
@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||
</List>
|
||||
)}
|
||||
|
||||
{queryConnectedCalendars.isLoading && (
|
||||
{queryIntegrations.isLoading && (
|
||||
<ul className="divide-y divide-gray-200 rounded-md border border-gray-200 bg-white p-0 dark:bg-black">
|
||||
{[0, 0, 0, 0].map((_item, index) => {
|
||||
return (
|
||||
|
|
|
@ -7,6 +7,7 @@ import { extendEventData, nextCollectBasicSettings } from "@calcom/lib/telemetry
|
|||
|
||||
const V2_WHITELIST = [
|
||||
"/settings/admin",
|
||||
"/settings/billing",
|
||||
"/settings/developer/webhooks",
|
||||
"/settings/developer/api-keys",
|
||||
"/settings/my-account",
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
{
|
||||
"name": "@calcom/web",
|
||||
"version": "2.0.2",
|
||||
"version": "2.0.4",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"analyze": "ANALYZE=true next build",
|
||||
|
|
|
@ -1,17 +1,16 @@
|
|||
import { GetServerSidePropsContext } from "next";
|
||||
import { signOut, useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
import { useEffect } from "react";
|
||||
|
||||
import { WEBSITE_URL } from "@calcom/lib/constants";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import Button from "@calcom/ui/Button";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
import { inferSSRProps } from "@lib/types/inferSSRProps";
|
||||
|
||||
import AuthContainer from "@components/ui/AuthContainer";
|
||||
import AuthContainer from "@components/v2/ui/AuthContainer";
|
||||
|
||||
import { ssrInit } from "@server/lib/ssr";
|
||||
|
||||
|
@ -30,7 +29,7 @@ export default function Logout(props: Props) {
|
|||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")}>
|
||||
<AuthContainer title={t("logged_out")} description={t("youve_been_logged_out")} showLogo>
|
||||
<div className="mb-4">
|
||||
<div className="mx-auto flex h-12 w-12 items-center justify-center rounded-full bg-green-100">
|
||||
<Icon.FiCheck className="h-6 w-6 text-green-600" />
|
||||
|
@ -44,9 +43,9 @@ export default function Logout(props: Props) {
|
|||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Link href="/auth/login" passHref>
|
||||
<Button className="flex w-full justify-center"> {t("go_back_login")}</Button>
|
||||
</Link>
|
||||
<Button href="/auth/login" passHref className="flex w-full justify-center">
|
||||
{t("go_back_login")}
|
||||
</Button>
|
||||
</AuthContainer>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -542,6 +542,7 @@ const EventTypesPage = () => {
|
|||
<title>Home | Cal.com</title>
|
||||
<link rel="icon" href="/favicon.ico" />
|
||||
</Head>
|
||||
|
||||
<Shell
|
||||
heading={t("event_types_page_title") as string}
|
||||
subtitle={t("event_types_page_subtitle") as string}
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||
import { UserPlan } from "@prisma/client";
|
||||
import Head from "next/head";
|
||||
import Link from "next/link";
|
||||
import { useRouter } from "next/router";
|
||||
|
@ -53,7 +52,7 @@ const Item = ({ type, group, readOnly }: { type: EventType; group: EventTypeGrou
|
|||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<Link href={`/event-types/${type.id}`}>
|
||||
<Link href={`/event-types/${type.id}?tabName=setup`}>
|
||||
<a
|
||||
className="flex-grow truncate text-sm"
|
||||
title={`${type.title} ${type.description ? `– ${type.description}` : ""}`}>
|
||||
|
@ -293,8 +292,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
<DropdownMenuItem>
|
||||
<DropdownItem
|
||||
type="button"
|
||||
href={"/event-types/" + type.id}
|
||||
StartIcon={Icon.FiEdit2}>
|
||||
data-testid={"event-type-edit-" + type.id}
|
||||
StartIcon={Icon.FiEdit2}
|
||||
onClick={() => router.push("/event-types/" + type.id)}>
|
||||
{t("edit") as string}
|
||||
</DropdownItem>
|
||||
</DropdownMenuItem>
|
||||
|
@ -397,9 +397,9 @@ export const EventTypeList = ({ group, groupIndex, readOnly, types }: EventTypeL
|
|||
<DropdownMenuItem className="outline-none">
|
||||
<Button
|
||||
type="button"
|
||||
href={"/event-types/" + type.id}
|
||||
onClick={() => router.push("/event-types/" + type.id)}
|
||||
color="minimal"
|
||||
className="w-full"
|
||||
className="w-full rounded-none"
|
||||
StartIcon={Icon.FiEdit}>
|
||||
{t("edit") as string}
|
||||
</Button>
|
||||
|
|
|
@ -0,0 +1,86 @@
|
|||
import { useState } from "react";
|
||||
import { HelpScout, useChat } from "react-live-chat-loader";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Button } from "@calcom/ui/v2";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
import { getLayout } from "@calcom/ui/v2/core/layouts/SettingsLayout";
|
||||
|
||||
interface CtaRowProps {
|
||||
title: string;
|
||||
description: string;
|
||||
children: React.ReactNode;
|
||||
className?: string;
|
||||
}
|
||||
|
||||
const CtaRow = ({ title, description, className, children }: CtaRowProps) => {
|
||||
return (
|
||||
<>
|
||||
<section className={classNames("flex flex-col sm:flex-row", className)}>
|
||||
<div>
|
||||
<h2 className="font-medium">{title}</h2>
|
||||
<p>{description}</p>
|
||||
</div>
|
||||
<div className="flex-shrink-0 pt-3 sm:ml-auto sm:pt-0 sm:pl-3">{children}</div>
|
||||
</section>
|
||||
<hr className="border-neutral-200" />
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
const BillingView = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: user } = trpc.useQuery(["viewer.me"]);
|
||||
const isPro = user?.plan === "PRO";
|
||||
const [, loadChat] = useChat();
|
||||
const [showChat, setShowChat] = useState(false);
|
||||
|
||||
const onContactSupportClick = () => {
|
||||
setShowChat(true);
|
||||
loadChat({ open: true });
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Meta title={t("billing")} description={t("manage_billing_description")} />
|
||||
<div className="space-y-6 text-sm sm:space-y-8">
|
||||
{!isPro && (
|
||||
<CtaRow title={t("billing_freeplan_title")} description={t("billing_freeplan_description")}>
|
||||
<form target="_blank" method="POST" action="/api/upgrade">
|
||||
<Button type="submit" EndIcon={Icon.FiExternalLink}>
|
||||
{t("billing_freeplan_cta")}
|
||||
</Button>
|
||||
</form>
|
||||
</CtaRow>
|
||||
)}
|
||||
|
||||
<CtaRow
|
||||
className={classNames(!isPro && "pointer-events-none opacity-30")}
|
||||
title={t("billing_manage_details_title")}
|
||||
description={t("billing_manage_details_description")}>
|
||||
<Button
|
||||
color={isPro ? "primary" : "secondary"}
|
||||
href="/api/integrations/stripepayment/portal"
|
||||
target="_blank"
|
||||
EndIcon={Icon.FiExternalLink}>
|
||||
{t("billing_portal")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
|
||||
<CtaRow title={t("billing_help_title")} description={t("billing_help_description")}>
|
||||
<Button color="secondary" onClick={onContactSupportClick}>
|
||||
{t("billing_help_cta")}
|
||||
</Button>
|
||||
</CtaRow>
|
||||
{showChat && <HelpScout color="#292929" icon="message" horizontalPosition="right" zIndex="1" />}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
BillingView.getLayout = getLayout;
|
||||
|
||||
export default BillingView;
|
|
@ -7,6 +7,7 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Alert } from "@calcom/ui/v2";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import EmptyScreen from "@calcom/ui/v2/core/EmptyScreen";
|
||||
import Meta from "@calcom/ui/v2/core/Meta";
|
||||
|
@ -23,7 +24,7 @@ import { CalendarSwitch } from "@components/v2/settings/CalendarSwitch";
|
|||
const SkeletonLoader = () => {
|
||||
return (
|
||||
<SkeletonContainer>
|
||||
<div className="mt-6 mb-8 space-y-6 divide-y">
|
||||
<div className="mt-6 mb-8 space-y-6">
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
<SkeletonText className="h-8 w-full" />
|
||||
|
@ -89,7 +90,30 @@ const CalendarsView = () => {
|
|||
<List>
|
||||
{data.connectedCalendars.map((item) => (
|
||||
<Fragment key={item.credentialId}>
|
||||
{item.calendars && (
|
||||
{item.error && item.error.message && (
|
||||
<Alert
|
||||
severity="warning"
|
||||
key={item.credentialId}
|
||||
title={t("calendar_connection_fail")}
|
||||
message={item.error.message}
|
||||
className="mb-4 mt-4"
|
||||
actions={
|
||||
<>
|
||||
{/* @TODO: add a reconnect button, that calls add api and delete old credential */}
|
||||
<DisconnectIntegration
|
||||
credentialId={item.credentialId}
|
||||
trashIcon
|
||||
onSuccess={() => query.refetch()}
|
||||
buttonProps={{
|
||||
className: "border border-gray-300 py-[2px]",
|
||||
color: "secondary",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
}
|
||||
/>
|
||||
)}
|
||||
{item?.error === undefined && item.calendars && (
|
||||
<ListItem expanded className="flex-col">
|
||||
<div className="flex w-full flex-1 items-center space-x-3 pb-5 pl-1 pt-1 rtl:space-x-reverse">
|
||||
{
|
||||
|
@ -154,6 +178,22 @@ const CalendarsView = () => {
|
|||
/>
|
||||
);
|
||||
}}
|
||||
error={() => {
|
||||
return (
|
||||
<Alert
|
||||
message={
|
||||
<Trans i18nKey="fetching_calendars_error">
|
||||
An error ocurred while fetching your Calendars.
|
||||
<a className="cursor-pointer underline" onClick={() => query.refetch()}>
|
||||
try again
|
||||
</a>
|
||||
.
|
||||
</Trans>
|
||||
}
|
||||
severity="error"
|
||||
/>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
|
|
|
@ -263,7 +263,7 @@ export default function Success(props: SuccessProps) {
|
|||
{userIsOwner && !isEmbed && (
|
||||
<div className="mt-2 ml-4 -mb-4">
|
||||
<Link href={eventType.recurringEvent?.count ? "/bookings/recurring" : "/bookings/upcoming"}>
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800">
|
||||
<a className="mt-2 inline-flex px-1 py-2 text-sm text-gray-500 hover:bg-gray-100 hover:text-gray-800 dark:hover:bg-transparent dark:hover:text-white">
|
||||
<Icon.FiChevronLeft className="h-5 w-5" /> {t("back_to_bookings")}
|
||||
</a>
|
||||
</Link>
|
||||
|
@ -411,7 +411,7 @@ export default function Success(props: SuccessProps) {
|
|||
!isCancelled &&
|
||||
(!isCancellationMode ? (
|
||||
<>
|
||||
<hr className="border-bookinglightest" />
|
||||
<hr className="border-bookinglightest dark:border-darkgray-300" />
|
||||
<div className="py-8 text-center last:pb-0">
|
||||
<span className="text-gray-900 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||
{t("need_to_make_a_change")}
|
||||
|
@ -448,7 +448,7 @@ export default function Success(props: SuccessProps) {
|
|||
))}
|
||||
{userIsOwner && !needsConfirmation && !isCancellationMode && !isCancelled && (
|
||||
<>
|
||||
<hr className="border-bookinglightest" />
|
||||
<hr className="border-bookinglightest dark:border-darkgray-300" />
|
||||
<div className="text-bookingdark align-center flex flex-row justify-center pt-8">
|
||||
<span className="flex self-center font-medium text-gray-700 ltr:mr-2 rtl:ml-2 dark:text-gray-50">
|
||||
{t("add_to_calendar")}
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import { expect } from "@playwright/test";
|
||||
|
||||
import { WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
||||
import { randomString } from "../lib/random";
|
||||
import { test } from "./lib/fixtures";
|
||||
|
||||
|
@ -87,7 +89,7 @@ test.describe("Event Types tests", () => {
|
|||
);
|
||||
const href = await firstElement.getAttribute("href");
|
||||
if (!href) throw new Error("No href found for event type");
|
||||
const [eventTypeId] = href.split("/").reverse();
|
||||
const [eventTypeId] = new URL(WEBAPP_URL + href).pathname.split("/").reverse();
|
||||
const firstTitle = await page.locator(`[data-testid=event-type-title-${eventTypeId}]`).innerText();
|
||||
const firstFullSlug = await page.locator(`[data-testid=event-type-slug-${eventTypeId}]`).innerText();
|
||||
const firstSlug = firstFullSlug.split("/")[2];
|
||||
|
|
|
@ -3,6 +3,7 @@ import type Prisma from "@prisma/client";
|
|||
import { Prisma as PrismaType, UserPlan } from "@prisma/client";
|
||||
import { hash } from "bcryptjs";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { DEFAULT_SCHEDULE, getAvailabilityFromSchedule } from "@calcom/lib/availability";
|
||||
import { prisma } from "@calcom/prisma";
|
||||
|
||||
|
@ -257,7 +258,7 @@ const createUser = async (
|
|||
password: await hashPassword(uname),
|
||||
emailVerified: new Date(),
|
||||
completedOnboarding: opts?.completedOnboarding ?? true,
|
||||
timeZone: opts?.timeZone ?? TimeZoneEnum.UK,
|
||||
timeZone: opts?.timeZone ?? dayjs.tz.guess(),
|
||||
locale: opts?.locale ?? "en",
|
||||
schedules:
|
||||
opts?.completedOnboarding ?? true
|
||||
|
|
|
@ -1253,9 +1253,9 @@
|
|||
"create_first_api_key_description": "API keys allow other apps to communicate with Cal.com",
|
||||
"back_to_signin": "Back to sign in",
|
||||
"reset_link_sent": "Reset link sent",
|
||||
"password_reset_email":"An email is on it’s way to {{email}} with instructions to reset your password.",
|
||||
"password_reset_leading":"If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.",
|
||||
"password_updated":"Password updated!",
|
||||
"password_reset_email": "An email is on it’s way to {{email}} with instructions to reset your password.",
|
||||
"password_reset_leading": "If you don't receive an email soon, check that the email address you entered is correct, check your spam folder or reach out to support if the issue persists.",
|
||||
"password_updated": "Password updated!",
|
||||
"pending_payment": "Pending payment",
|
||||
"confirmation_page_rainbow": "Token gate your event with tokens or NFTs on Ethereum, Polygon, and more.",
|
||||
"not_on_cal": "Not on Cal.com",
|
||||
|
@ -1272,5 +1272,28 @@
|
|||
"format": "Format",
|
||||
"uppercase_for_letters": "Use uppercase for all letters",
|
||||
"replace_whitespaces_underscores": "Replace whitespaces with underscores",
|
||||
"ignore_special_characters": "Ignore special characters in your Additonal Input label. Use only letters and numbers"
|
||||
"manage_billing": "Manage billing",
|
||||
"manage_billing_description": "Manage all things billing",
|
||||
"billing_freeplan_title": "You're currently on the FREE plan",
|
||||
"billing_freeplan_description": "We work better in teams. Extend your workflows with round-robin and collective events and make advanced routing forms",
|
||||
"billing_freeplan_cta": "Try now",
|
||||
"billing_manage_details_title": "View and manage your billing details",
|
||||
"billing_manage_details_description": "View and edit your billing details, as well as cancel your subscription.",
|
||||
"billing_portal": "Billing portal",
|
||||
"billing_help_title": "Need anything else?",
|
||||
"billing_help_description": "If you need any further help with billing, our support team are here to help.",
|
||||
"billing_help_cta": "Contact support",
|
||||
"ignore_special_characters": "Ignore special characters in your Additional Input label. Use only letters and numbers",
|
||||
"retry": "Retry",
|
||||
"fetching_calendars_error": "There was a problem fetching your calendars. Please <1>try again</1> or reach out to customer support.",
|
||||
"calendar_connection_fail": "Calendar connection failed",
|
||||
"booking_confirmation_success": "Booking confirmation succeeded",
|
||||
"booking_confirmation_fail": "Booking confirmation failed",
|
||||
"we_wont_show_again": "We won't show this again",
|
||||
"couldnt_update_timezone": "We couldn't update the timezone",
|
||||
"updated_timezone_to": "Updated timezone to {{formattedCurrentTz}}",
|
||||
"update_timezone": "Update timezone",
|
||||
"update_timezone_question": "Update Timezone?",
|
||||
"update_timezone_description": "It seems like your local timezone has changed to {{formattedCurrentTz}}. It's very important to have the correct timezone to prevent bookings at undesired times. Do you want to update it?",
|
||||
"dont_update": "Don't update"
|
||||
}
|
||||
|
|
|
@ -1 +1 @@
|
|||
Subproject commit 44367dcee988568ab479cf7d29abfb8ce4abe85a
|
||||
Subproject commit 329a8f3c4c9f3d9fb2b4097ce8fbfbf15a9b8477
|
|
@ -8,6 +8,7 @@ import getApps from "@calcom/app-store/utils";
|
|||
import { getUid } from "@calcom/lib/CalEventParser";
|
||||
import logger from "@calcom/lib/logger";
|
||||
import { performance } from "@calcom/lib/server/perfObserver";
|
||||
import { App } from "@calcom/types/App";
|
||||
import type { CalendarEvent, EventBusyDate, NewCalendarEventType } from "@calcom/types/Calendar";
|
||||
import type { EventResult } from "@calcom/types/EventManager";
|
||||
|
||||
|
@ -33,47 +34,85 @@ export const getConnectedCalendars = async (
|
|||
) => {
|
||||
const connectedCalendars = await Promise.all(
|
||||
calendarCredentials.map(async (item) => {
|
||||
const { calendar, integration, credential } = item;
|
||||
const credentialId = credential.id;
|
||||
if (!calendar) {
|
||||
try {
|
||||
const { calendar, integration, credential } = item;
|
||||
|
||||
// Don't leak credentials to the client
|
||||
const credentialId = credential.id;
|
||||
if (!calendar) {
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
};
|
||||
}
|
||||
const cals = await calendar.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
readOnly: cal.readOnly || false,
|
||||
primary: cal.primary || null,
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
credentialId,
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
||||
if (!primary) {
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
error: {
|
||||
message: "No primary calendar found",
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
integration,
|
||||
integration: cleanIntegrationKeys(integration),
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
}
|
||||
const cals = await calendar.listCalendars();
|
||||
const calendars = _(cals)
|
||||
.map((cal) => ({
|
||||
...cal,
|
||||
readOnly: cal.readOnly || false,
|
||||
primary: cal.primary || null,
|
||||
isSelected: selectedCalendars.some((selected) => selected.externalId === cal.externalId),
|
||||
credentialId,
|
||||
}))
|
||||
.sortBy(["primary"])
|
||||
.value();
|
||||
const primary = calendars.find((item) => item.primary) ?? calendars.find((cal) => cal !== undefined);
|
||||
if (!primary) {
|
||||
} catch (error) {
|
||||
let errorMessage = "Could not get connected calendars";
|
||||
|
||||
// Here you can expect for specific errors
|
||||
if (error instanceof Error) {
|
||||
if (error.message === "invalid_grant") {
|
||||
errorMessage = "Access token expired or revoked";
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
integration: cleanIntegrationKeys(item.integration),
|
||||
credentialId: item.credential.id,
|
||||
error: {
|
||||
message: "No primary calendar found",
|
||||
message: errorMessage,
|
||||
},
|
||||
};
|
||||
}
|
||||
return {
|
||||
integration,
|
||||
credentialId,
|
||||
primary,
|
||||
calendars,
|
||||
};
|
||||
})
|
||||
);
|
||||
|
||||
return connectedCalendars;
|
||||
};
|
||||
|
||||
/**
|
||||
* Important function to prevent leaking credentials to the client
|
||||
* @param appIntegration
|
||||
* @returns App
|
||||
*/
|
||||
const cleanIntegrationKeys = (
|
||||
appIntegration: ReturnType<typeof getCalendarCredentials>[number]["integration"] & {
|
||||
credentials?: Array<Credential>;
|
||||
credential: Credential;
|
||||
}
|
||||
) => {
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { credentials, credential, ...rest } = appIntegration;
|
||||
return rest;
|
||||
};
|
||||
|
||||
const CACHING_TIME = 30_000; // 30 seconds
|
||||
|
||||
const getCachedResults = async (
|
||||
|
|
|
@ -6,8 +6,10 @@ import { WEBAPP_URL } from "@calcom/lib/constants";
|
|||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { Icon } from "@calcom/ui";
|
||||
import { Button, Avatar, Badge } from "@calcom/ui/v2";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2";
|
||||
import Avatar from "@calcom/ui/v2/core/Avatar";
|
||||
import Badge from "@calcom/ui/v2/core/Badge";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
import { SkeletonContainer, SkeletonText } from "@calcom/ui/v2/core/skeleton";
|
||||
|
||||
const AddNewTeamMemberSkeleton = () => {
|
||||
return (
|
||||
|
@ -58,7 +60,7 @@ const AddNewTeamMembers = (props: { teamId: number }) => {
|
|||
<Avatar
|
||||
gravatarFallbackMd5="teamMember"
|
||||
size="mdLg"
|
||||
imageSrc={member?.avatar}
|
||||
imageSrc={WEBAPP_URL + "/" + member.username + "/avatar.png"}
|
||||
alt="owner-avatar"
|
||||
/>
|
||||
<div>
|
||||
|
|
|
@ -80,7 +80,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
|||
return (
|
||||
<>
|
||||
<div className="my-8 sm:my-0 md:flex">
|
||||
<div className="pl-2 pr-3 md:pl-0">
|
||||
<div className="pl-2 pr-3 md:sticky md:top-6 md:h-0 md:pl-0">
|
||||
<div className="mb-5">
|
||||
<TextField label={`${t("workflow_name")}:`} type="text" {...form.register("name")} />
|
||||
</div>
|
||||
|
@ -116,7 +116,7 @@ export default function WorkflowDetailsPage(props: Props) {
|
|||
</div>
|
||||
|
||||
{/* Workflow Trigger Event & Steps */}
|
||||
<div className="w-full rounded-md border border-gray-200 bg-gray-50 p-3 py-5 md:ml-3 md:max-h-[calc(100vh-116px)] md:overflow-scroll md:p-8">
|
||||
<div className="w-full rounded-md border border-gray-200 bg-gray-50 p-3 py-5 md:ml-3 md:p-8">
|
||||
{form.getValues("trigger") && (
|
||||
<div>
|
||||
<WorkflowStepContainer form={form} />
|
||||
|
|
|
@ -53,7 +53,9 @@ const WebhookForm = (props: {
|
|||
const triggerOptions = [...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2["core"]];
|
||||
if (apps) {
|
||||
for (const app of apps) {
|
||||
triggerOptions.push(...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2[app]);
|
||||
if (WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2[app]) {
|
||||
triggerOptions.push(...WEBHOOK_TRIGGER_EVENTS_GROUPED_BY_APP_V2[app]);
|
||||
}
|
||||
}
|
||||
}
|
||||
const translatedTriggerOptions = triggerOptions.map((option) => ({ ...option, label: t(option.label) }));
|
||||
|
|
|
@ -221,6 +221,20 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
}
|
||||
}
|
||||
|
||||
isValidFormat = (url: string): boolean => {
|
||||
const acceptedFormats = ["eml", "ics"];
|
||||
const urlFormat = url.split(".").pop();
|
||||
if (urlFormat === undefined) {
|
||||
console.error("Invalid request, calendar object extension missing");
|
||||
return false;
|
||||
}
|
||||
if (!acceptedFormats.includes(urlFormat)) {
|
||||
console.error(`Unsupported calendar object format: ${urlFormat}`);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
async getAvailability(
|
||||
dateFrom: string,
|
||||
dateTo: string,
|
||||
|
@ -232,6 +246,7 @@ export default abstract class BaseCalendarService implements Calendar {
|
|||
.filter((sc) => ["caldav_calendar", "apple_calendar"].includes(sc.integration ?? ""))
|
||||
.map((sc) =>
|
||||
fetchCalendarObjects({
|
||||
urlFilter: (url: string) => this.isValidFormat(url),
|
||||
calendar: {
|
||||
url: sc.externalId,
|
||||
},
|
||||
|
|
|
@ -12,7 +12,6 @@ export async function getTeamWithMembers(id?: number, slug?: string) {
|
|||
id: true,
|
||||
plan: true,
|
||||
bio: true,
|
||||
avatar: true,
|
||||
});
|
||||
|
||||
const teamSelect = Prisma.validator<Prisma.TeamSelect>()({
|
||||
|
|
|
@ -766,7 +766,7 @@ const loggedInViewerRouter = createProtectedRouter()
|
|||
const apps = getApps(credentials);
|
||||
const appFromDb = apps.find((app) => app.credential?.appId === appId);
|
||||
if (!appFromDb) {
|
||||
return appFromDb;
|
||||
return null;
|
||||
}
|
||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||
const { credential: _, credentials: _1, ...app } = appFromDb;
|
||||
|
|
|
@ -461,37 +461,48 @@ export const bookingsRouter = createProtectedRouter()
|
|||
}
|
||||
|
||||
//Workflows - set reminders for confirmed events
|
||||
for (const updatedBooking of updatedBookings) {
|
||||
if (updatedBooking.eventType?.workflows) {
|
||||
const evtOfBooking = evt;
|
||||
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
|
||||
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
|
||||
evtOfBooking.uid = updatedBooking.uid;
|
||||
|
||||
await scheduleWorkflowReminders(
|
||||
updatedBooking.eventType.workflows,
|
||||
updatedBooking.smsReminderNumber,
|
||||
evtOfBooking,
|
||||
false,
|
||||
false
|
||||
);
|
||||
try {
|
||||
for (const updatedBooking of updatedBookings) {
|
||||
if (updatedBooking.eventType?.workflows) {
|
||||
const evtOfBooking = evt;
|
||||
evtOfBooking.startTime = updatedBooking.startTime.toISOString();
|
||||
evtOfBooking.endTime = updatedBooking.endTime.toISOString();
|
||||
evtOfBooking.uid = updatedBooking.uid;
|
||||
|
||||
await scheduleWorkflowReminders(
|
||||
updatedBooking.eventType.workflows,
|
||||
updatedBooking.smsReminderNumber,
|
||||
evtOfBooking,
|
||||
false,
|
||||
false
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
// Silently fail
|
||||
console.error(error);
|
||||
}
|
||||
|
||||
// schedule job for zapier trigger 'when meeting ends'
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: booking.userId || 0,
|
||||
eventTypeId: booking.eventTypeId || 0,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
};
|
||||
try {
|
||||
// schedule job for zapier trigger 'when meeting ends'
|
||||
const subscriberOptionsMeetingEnded = {
|
||||
userId: booking.userId || 0,
|
||||
eventTypeId: booking.eventTypeId || 0,
|
||||
triggerEvent: WebhookTriggerEvents.MEETING_ENDED,
|
||||
};
|
||||
|
||||
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
||||
const subscribersMeetingEnded = await getWebhooks(subscriberOptionsMeetingEnded);
|
||||
|
||||
subscribersMeetingEnded.forEach((subscriber) => {
|
||||
updatedBookings.forEach((booking) => {
|
||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||
subscribersMeetingEnded.forEach((subscriber) => {
|
||||
updatedBookings.forEach((booking) => {
|
||||
scheduleTrigger(booking, subscriber.subscriberUrl, subscriber);
|
||||
});
|
||||
});
|
||||
});
|
||||
} catch (error) {
|
||||
// Silently fail
|
||||
console.error(error);
|
||||
}
|
||||
} else {
|
||||
evt.rejectionReason = rejectionReason;
|
||||
if (recurringEventId) {
|
||||
|
|
|
@ -0,0 +1,73 @@
|
|||
import { useState, useEffect } from "react";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import { DialogContent, showToast } from "@calcom/ui/v2";
|
||||
import { Dialog } from "@calcom/ui/v2/core/Dialog";
|
||||
|
||||
export default function TimezoneChangeDialog() {
|
||||
const { t } = useLocale();
|
||||
const { data: user, isLoading } = trpc.useQuery(["viewer.me"]);
|
||||
const utils = trpc.useContext();
|
||||
const userTz = user?.timeZone;
|
||||
const currentTz = dayjs.tz.guess();
|
||||
const formattedCurrentTz = currentTz?.replace("_", " ");
|
||||
|
||||
// update user settings
|
||||
const onSuccessMutation = async () => {
|
||||
showToast(t("updated_timezone_to", { formattedCurrentTz }), "success");
|
||||
await utils.invalidateQueries(["viewer.me"]);
|
||||
};
|
||||
|
||||
const onErrorMutation = () => {
|
||||
showToast(t("couldnt_update_timezone"), "error");
|
||||
};
|
||||
|
||||
// update timezone in db
|
||||
const mutation = trpc.useMutation("viewer.updateProfile", {
|
||||
onSuccess: onSuccessMutation,
|
||||
onError: onErrorMutation,
|
||||
});
|
||||
|
||||
function updateTimezone() {
|
||||
setOpen(false);
|
||||
mutation.mutate({
|
||||
timeZone: currentTz,
|
||||
});
|
||||
}
|
||||
|
||||
// check for difference in user timezone and current browser timezone
|
||||
const [open, setOpen] = useState(false);
|
||||
useEffect(() => {
|
||||
const tzDifferent =
|
||||
!isLoading && dayjs.tz(undefined, currentTz).utcOffset() !== dayjs.tz(undefined, userTz).utcOffset();
|
||||
const showDialog = tzDifferent && !document.cookie.includes("calcom-timezone-dialog=1");
|
||||
setOpen(showDialog);
|
||||
}, [currentTz, isLoading, userTz]);
|
||||
|
||||
// save cookie to not show again
|
||||
function onCancel(maxAge: number, toast: boolean) {
|
||||
setOpen(false);
|
||||
document.cookie = `calcom-timezone-dialog=1;max-age=${maxAge}`;
|
||||
toast && showToast(t("we_wont_show_again"), "success");
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={setOpen}>
|
||||
<DialogContent
|
||||
title={t("update_timezone_question")}
|
||||
description={t("update_timezone_description", { formattedCurrentTz })}
|
||||
type="creation"
|
||||
actionText={t("update_timezone")}
|
||||
actionOnClick={() => updateTimezone()}
|
||||
closeText={t("dont_update")}
|
||||
onInteractOutside={() => onCancel(86400, false) /* 1 day expire */}
|
||||
actionOnClose={() => onCancel(7776000, true) /* 3 months expire */}>
|
||||
{/* todo: save this in db and auto-update when timezone changes (be able to disable??? if yes, /settings)
|
||||
<Checkbox description="Always update timezone" />
|
||||
*/}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
|
@ -4,6 +4,7 @@ import React, { ReactNode, useState } from "react";
|
|||
import { Icon } from "react-feather";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import Button from "./Button";
|
||||
|
||||
|
@ -79,73 +80,77 @@ type DialogContentProps = React.ComponentProps<typeof DialogPrimitive["Content"]
|
|||
};
|
||||
|
||||
export const DialogContent = React.forwardRef<HTMLDivElement, DialogContentProps>(
|
||||
({ children, Icon, actionProps, ...props }, forwardedRef) => (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
{/*zIndex one less than Toast */}
|
||||
<DialogPrimitive.Content
|
||||
{...props}
|
||||
className={classNames(
|
||||
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
|
||||
props.size == "xl"
|
||||
? "p-8 sm:max-w-[90rem]"
|
||||
: props.size == "lg"
|
||||
? "p-8 sm:max-w-[70rem]"
|
||||
: props.size == "md"
|
||||
? "p-8 sm:max-w-[40rem]"
|
||||
: "p-8 sm:max-w-[35rem]",
|
||||
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
|
||||
`${props.className || ""}`
|
||||
)}
|
||||
ref={forwardedRef}>
|
||||
{props.type === "creation" && (
|
||||
<div>
|
||||
{props.title && <DialogHeader title={props.title} />}
|
||||
{props.description && <p className="pb-5 text-sm text-gray-500">{props.description}</p>}
|
||||
<div className="flex flex-col space-y-6">{children}</div>
|
||||
</div>
|
||||
)}
|
||||
{props.type === "confirmation" && (
|
||||
<div className="flex">
|
||||
{Icon && (
|
||||
<div className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
|
||||
<Icon className="h-4 w-4 text-black" />
|
||||
</div>
|
||||
)}
|
||||
({ children, title, Icon, actionProps, ...props }, forwardedRef) => {
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<DialogPrimitive.Portal>
|
||||
<DialogPrimitive.Overlay className="fadeIn fixed inset-0 z-40 bg-gray-500 bg-opacity-75 transition-opacity" />
|
||||
{/*zIndex one less than Toast */}
|
||||
<DialogPrimitive.Content
|
||||
{...props}
|
||||
className={classNames(
|
||||
"fadeIn fixed left-1/2 top-1/2 z-[9998] min-w-[360px] -translate-x-1/2 -translate-y-1/2 rounded bg-white text-left shadow-xl focus-visible:outline-none sm:w-full sm:align-middle",
|
||||
props.size == "xl"
|
||||
? "p-8 sm:max-w-[90rem]"
|
||||
: props.size == "lg"
|
||||
? "p-8 sm:max-w-[70rem]"
|
||||
: props.size == "md"
|
||||
? "p-8 sm:max-w-[40rem]"
|
||||
: "p-8 sm:max-w-[35rem]",
|
||||
"max-h-[560px] overflow-visible overscroll-auto md:h-auto md:max-h-[inherit]",
|
||||
`${props.className || ""}`
|
||||
)}
|
||||
ref={forwardedRef}>
|
||||
{props.type === "creation" && (
|
||||
<div>
|
||||
{props.title && <DialogHeader title={props.title} />}
|
||||
{props.description && <p className="mb-6 text-sm text-gray-500">{props.description}</p>}
|
||||
{title && <DialogHeader title={title} />}
|
||||
{props.description && <p className="pb-5 text-sm text-gray-500">{props.description}</p>}
|
||||
<div className="flex flex-col space-y-6">{children}</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{!props.useOwnActionButtons && (
|
||||
<DialogFooter>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<DialogClose asChild>
|
||||
{/* This will require the i18n string passed in */}
|
||||
<Button color="minimal" onClick={props.actionOnClose}>
|
||||
{props.closeText ?? "Close"}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{props.actionOnClick ? (
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={props.actionDisabled}
|
||||
onClick={props.actionOnClick}
|
||||
{...actionProps}>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
) : (
|
||||
<Button color="primary" type="submit" disabled={props.actionDisabled} {...actionProps}>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
)}
|
||||
{props.type === "confirmation" && (
|
||||
<div className="flex">
|
||||
{Icon && (
|
||||
<div className="mr-4 inline-flex h-10 w-10 items-center justify-center rounded-full bg-gray-300">
|
||||
<Icon className="h-4 w-4 text-black" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
{title && <DialogHeader title={title} />}
|
||||
{props.description && <p className="mb-6 text-sm text-gray-500">{props.description}</p>}
|
||||
</div>
|
||||
</div>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
)
|
||||
)}
|
||||
{!props.useOwnActionButtons && (
|
||||
<DialogFooter>
|
||||
<div className="mt-2 flex space-x-2">
|
||||
<DialogClose asChild>
|
||||
{/* This will require the i18n string passed in */}
|
||||
<Button color="minimal" onClick={props.actionOnClose}>
|
||||
{props.closeText ?? t("close")}
|
||||
</Button>
|
||||
</DialogClose>
|
||||
{props.actionOnClick ? (
|
||||
<Button
|
||||
color="primary"
|
||||
disabled={props.actionDisabled}
|
||||
onClick={props.actionOnClick}
|
||||
{...actionProps}>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
) : (
|
||||
<Button color="primary" type="submit" disabled={props.actionDisabled} {...actionProps}>
|
||||
{props.actionText}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</DialogFooter>
|
||||
)}
|
||||
</DialogPrimitive.Content>
|
||||
</DialogPrimitive.Portal>
|
||||
);
|
||||
}
|
||||
);
|
||||
|
||||
type DialogHeaderProps = {
|
||||
|
|
|
@ -129,7 +129,11 @@ export function ButtonOrLink({ href, ...props }: ButtonOrLinkProps) {
|
|||
const content = <ButtonOrLink {...props} />;
|
||||
|
||||
if (isLink) {
|
||||
return <Link href={href}>{content}</Link>;
|
||||
return (
|
||||
<Link href={href}>
|
||||
<a>{content}</a>
|
||||
</Link>
|
||||
);
|
||||
}
|
||||
|
||||
return content;
|
||||
|
|
|
@ -10,7 +10,6 @@ import dayjs from "@calcom/dayjs";
|
|||
import { useIsEmbed } from "@calcom/embed-core/embed-iframe";
|
||||
import ImpersonatingBanner from "@calcom/features/ee/impersonation/components/ImpersonatingBanner";
|
||||
import HelpMenuItem from "@calcom/features/ee/support/components/HelpMenuItem";
|
||||
import UserV2OptInBanner from "@calcom/features/users/components/UserV2OptInBanner";
|
||||
import CustomBranding from "@calcom/lib/CustomBranding";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { JOIN_SLACK, ROADMAP, DESKTOP_APP_LINK, WEBAPP_URL } from "@calcom/lib/constants";
|
||||
|
@ -27,6 +26,7 @@ import Dropdown, {
|
|||
DropdownMenuPortal,
|
||||
} from "@calcom/ui/Dropdown";
|
||||
import { Icon } from "@calcom/ui/Icon";
|
||||
import TimezoneChangeDialog from "@calcom/ui/TimezoneChangeDialog";
|
||||
import Button from "@calcom/ui/v2/core/Button";
|
||||
|
||||
/* TODO: Get this from endpoint */
|
||||
|
@ -136,6 +136,9 @@ const Layout = (props: LayoutProps) => {
|
|||
<Toaster position="bottom-right" />
|
||||
</div>
|
||||
|
||||
{/* todo: only run this if timezone is different */}
|
||||
<TimezoneChangeDialog />
|
||||
|
||||
<div className="flex h-screen overflow-hidden" data-testid="dashboard-shell">
|
||||
{props.SidebarContainer || <SideBarContainer />}
|
||||
<div className="flex w-0 flex-1 flex-col overflow-hidden">
|
||||
|
@ -783,7 +786,7 @@ export function ShellMain(props: LayoutProps) {
|
|||
<div
|
||||
className={classNames(
|
||||
props.backPath ? "relative" : "fixed right-4 bottom-[75px] z-40 ",
|
||||
"cta mb-4 flex-shrink-0 sm:relative sm:bottom-auto sm:right-auto sm:z-0"
|
||||
"cta mb-4 flex-shrink-0 sm:relative sm:bottom-auto sm:right-auto"
|
||||
)}>
|
||||
{props.CTA}
|
||||
</div>
|
||||
|
|
|
@ -166,6 +166,26 @@ type InputFieldProps = {
|
|||
labelClassName?: string;
|
||||
};
|
||||
|
||||
type AddonProps = {
|
||||
children: React.ReactNode;
|
||||
isFilled?: boolean;
|
||||
className?: string;
|
||||
error?: boolean;
|
||||
};
|
||||
|
||||
const Addon = ({ isFilled, children, className, error }: AddonProps) => (
|
||||
<div
|
||||
className={classNames(
|
||||
"addon-wrapper h-9 border border-gray-300 px-3",
|
||||
isFilled && "bg-gray-100",
|
||||
className
|
||||
)}>
|
||||
<div className={classNames("flex h-full flex-col justify-center px-1 text-sm", error && "text-red-900")}>
|
||||
<span className="whitespace-nowrap py-2.5">{children}</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
||||
const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputField(props, ref) {
|
||||
const id = useId();
|
||||
const { t: _t, isLocaleReady, i18n } = useLocale();
|
||||
|
@ -202,26 +222,12 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
</Skeleton>
|
||||
)}
|
||||
{addOnLeading || addOnSuffix ? (
|
||||
<div
|
||||
className={classNames(
|
||||
" mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1",
|
||||
addOnSuffix && "group flex-row-reverse"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"h-9 border border-gray-300",
|
||||
addOnFilled && "bg-gray-100",
|
||||
addOnLeading && "rounded-l-md border-r-0 px-3",
|
||||
addOnSuffix && "rounded-r-md border-l-0"
|
||||
)}>
|
||||
<div
|
||||
className={classNames(
|
||||
"flex h-full flex-col justify-center px-1 text-sm",
|
||||
props.error && "text-red-900"
|
||||
)}>
|
||||
<span className="whitespace-nowrap py-2.5">{addOnLeading || addOnSuffix}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="relative mb-1 flex items-center rounded-md focus-within:outline-none focus-within:ring-2 focus-within:ring-neutral-800 focus-within:ring-offset-1">
|
||||
{addOnLeading && (
|
||||
<Addon isFilled={addOnFilled} className="rounded-l-md border-r-0">
|
||||
{addOnLeading}
|
||||
</Addon>
|
||||
)}
|
||||
<Input
|
||||
id={id}
|
||||
placeholder={placeholder}
|
||||
|
@ -234,6 +240,11 @@ const InputField = forwardRef<HTMLInputElement, InputFieldProps>(function InputF
|
|||
{...passThrough}
|
||||
ref={ref}
|
||||
/>
|
||||
{addOnSuffix && (
|
||||
<Addon isFilled={addOnFilled} className="rounded-r-md border-l-0">
|
||||
{addOnSuffix}
|
||||
</Addon>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<Input id={id} placeholder={placeholder} className={className} {...passThrough} ref={ref} />
|
||||
|
@ -252,40 +263,39 @@ export const PasswordField = forwardRef<HTMLInputElement, InputFieldProps>(funct
|
|||
props,
|
||||
ref
|
||||
) {
|
||||
/*const { t } = useLocale();
|
||||
const { t } = useLocale();
|
||||
const [isPasswordVisible, setIsPasswordVisible] = useState(false);
|
||||
const toggleIsPasswordVisible = useCallback(
|
||||
() => setIsPasswordVisible(!isPasswordVisible),
|
||||
[isPasswordVisible, setIsPasswordVisible]
|
||||
);
|
||||
const textLabel = isPasswordVisible ? t("hide_password") : t("show_password");
|
||||
*/
|
||||
|
||||
return (
|
||||
<div className="relative">
|
||||
<div className="relative [&_.group:hover_.addon-wrapper]:border-gray-400 [&_.group:focus-within_.addon-wrapper]:border-neutral-300">
|
||||
<InputField
|
||||
type={/* isPasswordVisible ? "text" : */ "password"}
|
||||
placeholder={/* isPasswordVisible ? "0hMy4P4ssw0rd" : */ "•••••••••••••"}
|
||||
type={isPasswordVisible ? "text" : "password"}
|
||||
placeholder={props.placeholder || "•••••••••••••"}
|
||||
ref={ref}
|
||||
{...props}
|
||||
className={classNames("mb-0 pr-10", props.className)}
|
||||
className={classNames("mb-0 border-r-0 pr-10", props.className)}
|
||||
addOnFilled={false}
|
||||
addOnSuffix={
|
||||
<Tooltip content={textLabel}>
|
||||
<button
|
||||
className="absolute right-3 bottom-0 h-9 text-gray-900"
|
||||
type="button"
|
||||
onClick={() => toggleIsPasswordVisible()}>
|
||||
{isPasswordVisible ? (
|
||||
<EyeOff className="h-4 stroke-[2.5px]" />
|
||||
) : (
|
||||
<Eye className="h-4 stroke-[2.5px]" />
|
||||
)}
|
||||
<span className="sr-only">{textLabel}</span>
|
||||
</button>
|
||||
</Tooltip>
|
||||
}
|
||||
/>
|
||||
|
||||
{/*<Tooltip content={textLabel}>
|
||||
<button
|
||||
className={classNames(
|
||||
"absolute right-3 h-9 text-gray-900",
|
||||
props.hintErrors ? "top-[22px]" : "bottom-0"
|
||||
)}
|
||||
type="button"
|
||||
onClick={() => toggleIsPasswordVisible()}>
|
||||
{isPasswordVisible ? (
|
||||
<EyeOff className="h-4 stroke-[2.5px]" />
|
||||
) : (
|
||||
<Eye className="h-4 stroke-[2.5px]" />
|
||||
)}
|
||||
<span className="sr-only">{textLabel}</span>
|
||||
</button>
|
||||
</Tooltip>*/}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
|
|
@ -47,9 +47,7 @@ const tabs: VerticalTabItemProps[] = [
|
|||
name: "billing",
|
||||
href: "/settings/billing",
|
||||
icon: Icon.FiCreditCard,
|
||||
children: [
|
||||
{ name: "Manage Billing", href: "/api/integrations/stripepayment/portal", isExternalLink: true },
|
||||
],
|
||||
children: [{ name: "manage_billing", href: "/settings/billing" }],
|
||||
},
|
||||
{
|
||||
name: "developer",
|
||||
|
@ -329,7 +327,7 @@ export default function SettingsLayout({
|
|||
<MobileSettingsContainer onSideContainerOpen={() => setSideContainerOpen(!sideContainerOpen)} />
|
||||
}>
|
||||
<div className="flex flex-1 [&>*]:flex-1">
|
||||
<div className="mx-auto max-w-3xl justify-center">
|
||||
<div className="mx-auto max-w-full justify-center md:max-w-3xl">
|
||||
<ShellHeader />
|
||||
<ErrorBoundary>{children}</ErrorBoundary>
|
||||
</div>
|
||||
|
|
Loading…
Reference in New Issue
Block a user