Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9b22f44cba | ||
|
|
c84f75df99 | ||
|
|
8f9a4fc78f | ||
|
|
7b30ade284 | ||
|
|
24a4646fdc | ||
|
|
0860036607 | ||
|
|
d080a0ef2d | ||
|
|
a6eab74fd1 | ||
|
|
0a2e275908 | ||
|
|
3b05ebc5b5 | ||
|
|
e5b4d2fb80 | ||
|
|
00a2a3d8a8 | ||
|
|
30ba168fc3 | ||
|
|
12eec186b2 | ||
|
|
4417d649ea | ||
|
|
7058da70e6 | ||
|
|
2cb383c337 | ||
|
|
fe5eca8eb3 | ||
|
|
7c6468953a | ||
|
|
28a0553d18 | ||
|
|
ffcb04fa99 |
No files matched your search
@@ -13,6 +13,12 @@
|
|||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"include": ["next-env.d.ts", "**/*.ts", "**/*.tsx", ".next/types/**/*.ts"],
|
"include": [
|
||||||
|
"next-env.d.ts",
|
||||||
|
"**/*.ts",
|
||||||
|
"**/*.tsx",
|
||||||
|
".next/types/**/*.ts",
|
||||||
|
"../../packages/types/window.d.ts"
|
||||||
|
],
|
||||||
"exclude": ["node_modules"]
|
"exclude": ["node_modules"]
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
import prismock from "../../../../../tests/libs/__mocks__/prisma";
|
||||||
|
|
||||||
|
import type { Request, Response } from "express";
|
||||||
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
|
import { createMocks } from "node-mocks-http";
|
||||||
|
import { describe, expect, test } from "vitest";
|
||||||
|
|
||||||
|
import dayjs from "@calcom/dayjs";
|
||||||
|
|
||||||
|
import handler from "./_get";
|
||||||
|
|
||||||
|
type CustomNextApiRequest = NextApiRequest & Request;
|
||||||
|
type CustomNextApiResponse = NextApiResponse & Response;
|
||||||
|
|
||||||
|
function buildMockData() {
|
||||||
|
prismock.user.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
username: "test",
|
||||||
|
name: "Test User",
|
||||||
|
email: "test@example.com",
|
||||||
|
},
|
||||||
|
});
|
||||||
|
prismock.eventType.create({
|
||||||
|
data: {
|
||||||
|
id: 1,
|
||||||
|
slug: "test",
|
||||||
|
length: 30,
|
||||||
|
title: "Test Event Type",
|
||||||
|
userId: 1,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
describe("GET /api/slots", () => {
|
||||||
|
describe("Errors", () => {
|
||||||
|
test("Missing required data", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
|
||||||
|
await handler(req, res);
|
||||||
|
|
||||||
|
expect(res.statusCode).toBe(400);
|
||||||
|
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"message": "invalid_type in 'startTime': Required; invalid_type in 'endTime': Required",
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe("Success", () => {
|
||||||
|
describe("Regular event-type", () => {
|
||||||
|
test("Returns and event type available slots", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
startTime: dayjs().format(),
|
||||||
|
endTime: dayjs().add(1, "day").format(),
|
||||||
|
usernameList: "test",
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
buildMockData();
|
||||||
|
await handler(req, res);
|
||||||
|
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||||
|
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"slots": {},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
test("Returns and event type available slots with passed timeZone", async () => {
|
||||||
|
const { req, res } = createMocks<CustomNextApiRequest, CustomNextApiResponse>({
|
||||||
|
method: "GET",
|
||||||
|
query: {
|
||||||
|
eventTypeId: 1,
|
||||||
|
startTime: dayjs().format(),
|
||||||
|
endTime: dayjs().add(1, "day").format(),
|
||||||
|
usernameList: "test",
|
||||||
|
timeZone: "UTC",
|
||||||
|
},
|
||||||
|
prisma: prismock,
|
||||||
|
});
|
||||||
|
buildMockData();
|
||||||
|
await handler(req, res);
|
||||||
|
console.log({ statusCode: res._getStatusCode(), data: JSON.parse(res._getData()) });
|
||||||
|
expect(JSON.parse(res._getData())).toMatchInlineSnapshot(`
|
||||||
|
{
|
||||||
|
"slots": {},
|
||||||
|
}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
@@ -38,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
|
|||||||
},
|
},
|
||||||
{}
|
{}
|
||||||
)
|
)
|
||||||
: availableSlots;
|
: availableSlots.slots;
|
||||||
|
|
||||||
return { slots: slotsInProvidedTimeZone };
|
return { slots: slotsInProvidedTimeZone };
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
|
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
|
||||||
import { useMemo } from "react";
|
import { useMemo } from "react";
|
||||||
|
|
||||||
import type { DataTransformer } from "@trpc/server";
|
type DataTransformer = {
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
serialize: (data: any) => any;
|
||||||
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
|
deserialize: (data: any) => any;
|
||||||
|
};
|
||||||
|
|
||||||
export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
export function createHydrateClient(opts: { transformer: DataTransformer }) {
|
||||||
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
return function HydrateClient(props: { children: React.ReactNode; state: DehydratedState }) {
|
||||||
const { state, children } = props;
|
const { state, children } = props;
|
||||||
|
|
||||||
@@ -16,6 +21,6 @@ export function createHydrateClient(opts: { transformer?: DataTransformer }) {
|
|||||||
return state;
|
return state;
|
||||||
}, [state]);
|
}, [state]);
|
||||||
|
|
||||||
return <Hydrate state={transformedState}>{children}</Hydrate>;
|
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -4,10 +4,7 @@ import { trpc } from "app/_trpc/client";
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import superjson from "superjson";
|
import superjson from "superjson";
|
||||||
|
|
||||||
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
|
import { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
|
||||||
import { httpLink } from "@calcom/trpc/client/links/httpLink";
|
|
||||||
import { loggerLink } from "@calcom/trpc/client/links/loggerLink";
|
|
||||||
import { splitLink } from "@calcom/trpc/client/links/splitLink";
|
|
||||||
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
import { ENDPOINTS } from "@calcom/trpc/react/shared";
|
||||||
|
|
||||||
export type Endpoint = (typeof ENDPOINTS)[number];
|
export type Endpoint = (typeof ENDPOINTS)[number];
|
||||||
|
|||||||
@@ -0,0 +1,12 @@
|
|||||||
|
import Setup from "@pages/auth/setup";
|
||||||
|
import { withAppDirSsr } from "app/WithAppDirSsr";
|
||||||
|
import { WithLayout } from "app/layoutHOC";
|
||||||
|
import type { InferGetServerSidePropsType } from "next";
|
||||||
|
|
||||||
|
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||||
|
|
||||||
|
export default WithLayout({
|
||||||
|
getLayout: null,
|
||||||
|
Page: Setup,
|
||||||
|
getData: withAppDirSsr<InferGetServerSidePropsType<typeof getServerSideProps>>(getServerSideProps),
|
||||||
|
})<"P">;
|
||||||
@@ -16,10 +16,10 @@ import { Plus } from "@calcom/ui/components/icon";
|
|||||||
import { QueryCell } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
interface AdditionalCalendarSelectorProps {
|
interface AdditionalCalendarSelectorProps {
|
||||||
isLoading?: boolean;
|
isPending?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
|
|||||||
return (
|
return (
|
||||||
<Dropdown modal={false}>
|
<Dropdown modal={false}>
|
||||||
<DropdownMenuTrigger asChild>
|
<DropdownMenuTrigger asChild>
|
||||||
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
|
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
|
||||||
{t("add")}
|
{t("add")}
|
||||||
</Button>
|
</Button>
|
||||||
</DropdownMenuTrigger>
|
</DropdownMenuTrigger>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import type { IframeHTMLAttributes } from "react";
|
import type { IframeHTMLAttributes } from "react";
|
||||||
import React, { useState } from "react";
|
import React, { useState, useEffect } from "react";
|
||||||
|
|
||||||
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
import useAddAppMutation from "@calcom/app-store/_utils/useAddAppMutation";
|
||||||
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
import { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
|
||||||
@@ -92,17 +92,19 @@ export const AppPage = ({
|
|||||||
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
|
||||||
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
|
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
|
||||||
|
|
||||||
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
|
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
|
||||||
{ appType: type },
|
|
||||||
{
|
useEffect(
|
||||||
onSettled(data) {
|
function refactorMeWithoutEffect() {
|
||||||
const credentialsCount = data?.credentials.length || 0;
|
const data = appDbQuery.data;
|
||||||
setShowDisconnectIntegration(
|
|
||||||
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
|
const credentialsCount = data?.credentials.length || 0;
|
||||||
);
|
setShowDisconnectIntegration(
|
||||||
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
|
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
|
||||||
},
|
);
|
||||||
}
|
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
|
||||||
|
},
|
||||||
|
[appDbQuery.data]
|
||||||
);
|
);
|
||||||
|
|
||||||
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
const dependencyData = trpc.viewer.appsRouter.queryForDependencies.useQuery(dependencies, {
|
||||||
@@ -190,7 +192,7 @@ export const AppPage = ({
|
|||||||
)}
|
)}
|
||||||
</header>
|
</header>
|
||||||
</div>
|
</div>
|
||||||
{!appDbQuery.isLoading ? (
|
{!appDbQuery.isPending ? (
|
||||||
isGlobal ||
|
isGlobal ||
|
||||||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
|
||||||
<div className="flex space-x-3">
|
<div className="flex space-x-3">
|
||||||
@@ -211,7 +213,7 @@ export const AppPage = ({
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type, variant, slug });
|
mutation.mutate({ type, variant, slug });
|
||||||
},
|
},
|
||||||
loading: mutation.isLoading,
|
loading: mutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -250,7 +252,7 @@ export const AppPage = ({
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type, variant, slug });
|
mutation.mutate({ type, variant, slug });
|
||||||
},
|
},
|
||||||
loading: mutation.isLoading,
|
loading: mutation.isPending,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return (
|
return (
|
||||||
@@ -272,7 +274,7 @@ export const AppPage = ({
|
|||||||
)}
|
)}
|
||||||
|
|
||||||
{dependencies &&
|
{dependencies &&
|
||||||
(!dependencyData.isLoading ? (
|
(!dependencyData.isPending ? (
|
||||||
<div className="mt-6">
|
<div className="mt-6">
|
||||||
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ type Props = {
|
|||||||
onChanged: () => unknown | Promise<unknown>;
|
onChanged: () => unknown | Promise<unknown>;
|
||||||
fromOnboarding?: boolean;
|
fromOnboarding?: boolean;
|
||||||
destinationCalendarId?: string;
|
destinationCalendarId?: string;
|
||||||
isLoading?: boolean;
|
isPending?: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
function CalendarList(props: Props) {
|
function CalendarList(props: Props) {
|
||||||
@@ -74,7 +74,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
suspense: true,
|
suspense: true,
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
});
|
});
|
||||||
const { fromOnboarding, isLoading } = props;
|
const { fromOnboarding, isPending } = props;
|
||||||
return (
|
return (
|
||||||
<QueryCell
|
<QueryCell
|
||||||
query={query}
|
query={query}
|
||||||
@@ -97,7 +97,7 @@ function ConnectedCalendarsList(props: Props) {
|
|||||||
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
<div className="flex flex-col xl:flex-row xl:space-x-5">
|
||||||
{!!data.connectedCalendars.length && (
|
{!!data.connectedCalendars.length && (
|
||||||
<div className="flex items-center">
|
<div className="flex items-center">
|
||||||
<AdditionalCalendarSelector isLoading={isLoading} />
|
<AdditionalCalendarSelector isPending={isPending} />
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
@@ -229,7 +229,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
hidePlaceholder
|
hidePlaceholder
|
||||||
value={data.destinationCalendar?.externalId}
|
value={data.destinationCalendar?.externalId}
|
||||||
onChange={mutation.mutate}
|
onChange={mutation.mutate}
|
||||||
isLoading={mutation.isLoading}
|
isPending={mutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -239,7 +239,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
|
|||||||
onChanged={onChanged}
|
onChanged={onChanged}
|
||||||
fromOnboarding={fromOnboarding}
|
fromOnboarding={fromOnboarding}
|
||||||
destinationCalendarId={data.destinationCalendar?.externalId}
|
destinationCalendarId={data.destinationCalendar?.externalId}
|
||||||
isLoading={mutation.isLoading}
|
isPending={mutation.isPending}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -100,9 +100,9 @@ export const InstallAppButtonChild = ({
|
|||||||
<DropdownMenuContent
|
<DropdownMenuContent
|
||||||
className="w-auto"
|
className="w-auto"
|
||||||
onInteractOutside={(event) => {
|
onInteractOutside={(event) => {
|
||||||
if (mutation.isLoading) event.preventDefault();
|
if (mutation.isPending) event.preventDefault();
|
||||||
}}>
|
}}>
|
||||||
{mutation.isLoading && (
|
{mutation.isPending && (
|
||||||
<div className="z-1 fixed inset-0 flex items-center justify-center">
|
<div className="z-1 fixed inset-0 flex items-center justify-center">
|
||||||
<Spinner />
|
<Spinner />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -139,7 +139,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
setRejectionDialogIsOpen(true);
|
setRejectionDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
icon: Ban,
|
icon: Ban,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isPending,
|
||||||
},
|
},
|
||||||
// For bookings with payment, only confirm if the booking is paid for
|
// For bookings with payment, only confirm if the booking is paid for
|
||||||
...((isPending && !paymentAppData.enabled) ||
|
...((isPending && !paymentAppData.enabled) ||
|
||||||
@@ -153,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
bookingConfirm(true);
|
bookingConfirm(true);
|
||||||
},
|
},
|
||||||
icon: Check,
|
icon: Check,
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isPending,
|
||||||
},
|
},
|
||||||
]
|
]
|
||||||
: []),
|
: []),
|
||||||
@@ -293,7 +293,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
setViewRecordingsDialogIsOpen(true);
|
setViewRecordingsDialogIsOpen(true);
|
||||||
},
|
},
|
||||||
color: showCheckRecordingButton ? "secondary" : "primary",
|
color: showCheckRecordingButton ? "secondary" : "primary",
|
||||||
disabled: mutation.isLoading,
|
disabled: mutation.isPending,
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
@@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button
|
<Button
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isPending}
|
||||||
data-testid="rejection-confirm"
|
data-testid="rejection-confirm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
bookingConfirm(false);
|
bookingConfirm(false);
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button
|
<Button
|
||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={chargeCardMutation.isLoading || chargeError}
|
disabled={chargeCardMutation.isPending || chargeError}
|
||||||
onClick={() =>
|
onClick={() =>
|
||||||
chargeCardMutation.mutate({
|
chargeCardMutation.mutate({
|
||||||
bookingId,
|
bookingId,
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
|
||||||
const [rescheduleReason, setRescheduleReason] = useState("");
|
const [rescheduleReason, setRescheduleReason] = useState("");
|
||||||
|
|
||||||
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
|
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
showToast(t("reschedule_request_sent"), "success");
|
showToast(t("reschedule_request_sent"), "success");
|
||||||
setIsOpenDialog(false);
|
setIsOpenDialog(false);
|
||||||
@@ -65,7 +65,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
|
|||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button
|
<Button
|
||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
rescheduleApi({
|
rescheduleApi({
|
||||||
bookingId,
|
bookingId,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ import type { EditableSchema } from "@calcom/features/form-builder/schema";
|
|||||||
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
import { APP_NAME, CAL_URL } from "@calcom/lib/constants";
|
||||||
|
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/lib/constants";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { Prisma } from "@calcom/prisma/client";
|
import type { Prisma } from "@calcom/prisma/client";
|
||||||
import { trpc } from "@calcom/trpc/react";
|
import { trpc } from "@calcom/trpc/react";
|
||||||
@@ -35,7 +36,6 @@ import {
|
|||||||
Tooltip,
|
Tooltip,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
|
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
|
||||||
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
|
|
||||||
|
|
||||||
import RequiresConfirmationController from "./RequiresConfirmationController";
|
import RequiresConfirmationController from "./RequiresConfirmationController";
|
||||||
|
|
||||||
@@ -55,8 +55,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
const [showEventNameTip, setShowEventNameTip] = useState(false);
|
||||||
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
|
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
|
||||||
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
|
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
|
||||||
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
|
||||||
|
|
||||||
const bookingFields: Prisma.JsonObject = {};
|
const bookingFields: Prisma.JsonObject = {};
|
||||||
@@ -66,27 +66,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
user?.theme === "dark" ||
|
user?.theme === "dark" ||
|
||||||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
|
||||||
|
|
||||||
eventType.bookingFields.forEach(({ name }) => {
|
formMethods.getValues().bookingFields.forEach(({ name }) => {
|
||||||
bookingFields[name] = `${name} input`;
|
bookingFields[name] = `${name} input`;
|
||||||
});
|
});
|
||||||
|
|
||||||
const eventNameObject: EventNameObjectType = {
|
const eventNameObject: EventNameObjectType = {
|
||||||
attendeeName: t("scheduler"),
|
attendeeName: t("scheduler"),
|
||||||
eventType: eventType.title,
|
eventType: formMethods.getValues("title"),
|
||||||
eventName: eventType.eventName,
|
eventName: formMethods.getValues("eventName"),
|
||||||
host: eventType.users[0]?.name || "Nameless",
|
host: formMethods.getValues("users")[0]?.name || "Nameless",
|
||||||
bookingFields: bookingFields,
|
bookingFields: bookingFields,
|
||||||
t,
|
t,
|
||||||
};
|
};
|
||||||
|
|
||||||
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
|
const [requiresConfirmation, setRequiresConfirmation] = useState(
|
||||||
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
|
formMethods.getValues("requiresConfirmation")
|
||||||
|
);
|
||||||
|
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
const noShowFeeEnabled = eventType.metadata?.apps?.stripe?.paymentOption === "HOLD";
|
const noShowFeeEnabled =
|
||||||
|
formMethods.getValues("metadata")?.apps?.stripe?.enabled === true &&
|
||||||
|
formMethods.getValues("metadata")?.apps?.stripe?.paymentOption === "HOLD";
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
|
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
|
||||||
}, [eventType.users, hashedUrl, team?.id]);
|
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
|
||||||
|
|
||||||
const toggleGuests = (enabled: boolean) => {
|
const toggleGuests = (enabled: boolean) => {
|
||||||
const bookingFields = formMethods.getValues("bookingFields");
|
const bookingFields = formMethods.getValues("bookingFields");
|
||||||
@@ -108,7 +112,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
};
|
};
|
||||||
|
|
||||||
const { shouldLockDisableProps } = useLockedFieldsManager(
|
const { shouldLockDisableProps } = useLockedFieldsManager(
|
||||||
eventType,
|
formMethods.getValues(),
|
||||||
t("locked_fields_admin_description"),
|
t("locked_fields_admin_description"),
|
||||||
t("locked_fields_member_description")
|
t("locked_fields_member_description")
|
||||||
);
|
);
|
||||||
@@ -122,7 +126,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
const closeEventNameTip = () => setShowEventNameTip(false);
|
const closeEventNameTip = () => setShowEventNameTip(false);
|
||||||
|
|
||||||
const setEventName = (value: string) => formMethods.setValue("eventName", value);
|
|
||||||
return (
|
return (
|
||||||
<div className="flex flex-col space-y-4">
|
<div className="flex flex-col space-y-4">
|
||||||
{/**
|
{/**
|
||||||
@@ -145,12 +148,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
</Link>
|
</Link>
|
||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
control={formMethods.control}
|
|
||||||
name="destinationCalendar"
|
name="destinationCalendar"
|
||||||
defaultValue={eventType.destinationCalendar || undefined}
|
|
||||||
render={({ field: { onChange, value } }) => (
|
render={({ field: { onChange, value } }) => (
|
||||||
<DestinationCalendarSelector
|
<DestinationCalendarSelector
|
||||||
destinationCalendar={eventType.destinationCalendar}
|
|
||||||
value={value ? value.externalId : undefined}
|
value={value ? value.externalId : undefined}
|
||||||
onChange={onChange}
|
onChange={onChange}
|
||||||
hidePlaceholder
|
hidePlaceholder
|
||||||
@@ -167,7 +167,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
type="text"
|
type="text"
|
||||||
{...shouldLockDisableProps("eventName")}
|
{...shouldLockDisableProps("eventName")}
|
||||||
placeholder={eventNamePlaceholder}
|
placeholder={eventNamePlaceholder}
|
||||||
defaultValue={eventType.eventName || ""}
|
|
||||||
{...formMethods.register("eventName")}
|
{...formMethods.register("eventName")}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Button
|
<Button
|
||||||
@@ -194,7 +193,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
{...shouldLockDisableProps("bookingFields")}
|
{...shouldLockDisableProps("bookingFields")}
|
||||||
dataStore={{
|
dataStore={{
|
||||||
options: {
|
options: {
|
||||||
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
|
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -203,15 +202,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<RequiresConfirmationController
|
<RequiresConfirmationController
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
seatsEnabled={seatsEnabled}
|
seatsEnabled={seatsEnabled}
|
||||||
metadata={eventType.metadata}
|
metadata={formMethods.getValues("metadata")}
|
||||||
requiresConfirmation={requiresConfirmation}
|
requiresConfirmation={requiresConfirmation}
|
||||||
onRequiresConfirmation={setRequiresConfirmation}
|
onRequiresConfirmation={setRequiresConfirmation}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="requiresBookerEmailVerification"
|
name="requiresBookerEmailVerification"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.requiresBookerEmailVerification}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
labelClassName="text-sm"
|
labelClassName="text-sm"
|
||||||
@@ -228,8 +225,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="hideCalendarNotes"
|
name="hideCalendarNotes"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.hideCalendarNotes}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
labelClassName="text-sm"
|
labelClassName="text-sm"
|
||||||
@@ -246,7 +241,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="successRedirectUrl"
|
name="successRedirectUrl"
|
||||||
control={formMethods.control}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
@@ -274,7 +268,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
placeholder={t("external_redirect_url")}
|
placeholder={t("external_redirect_url")}
|
||||||
required={redirectUrlVisible}
|
required={redirectUrlVisible}
|
||||||
type="text"
|
type="text"
|
||||||
defaultValue={eventType.successRedirectUrl || ""}
|
|
||||||
{...formMethods.register("successRedirectUrl")}
|
{...formMethods.register("successRedirectUrl")}
|
||||||
/>
|
/>
|
||||||
<div
|
<div
|
||||||
@@ -327,7 +320,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
hint={t("private_link_hint")}
|
hint={t("private_link_hint")}
|
||||||
defaultValue={placeholderHashedLink}
|
defaultValue={placeholderHashedLink}
|
||||||
addOnSuffix={
|
addOnSuffix={
|
||||||
<Tooltip content={eventType.hashedLink ? t("copy_to_clipboard") : t("enabled_after_update")}>
|
<Tooltip
|
||||||
|
content={
|
||||||
|
formMethods.getValues("hashedLink") ? t("copy_to_clipboard") : t("enabled_after_update")
|
||||||
|
}>
|
||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
size="sm"
|
size="sm"
|
||||||
@@ -336,7 +332,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
aria-label="copy link"
|
aria-label="copy link"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
navigator.clipboard.writeText(placeholderHashedLink);
|
navigator.clipboard.writeText(placeholderHashedLink);
|
||||||
if (eventType.hashedLink) {
|
if (formMethods.getValues("hashedLink")) {
|
||||||
showToast(t("private_link_copied"), "success");
|
showToast(t("private_link_copied"), "success");
|
||||||
} else {
|
} else {
|
||||||
showToast(t("enabled_after_update_description"), "warning");
|
showToast(t("enabled_after_update_description"), "warning");
|
||||||
@@ -353,8 +349,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
|
|
||||||
<Controller
|
<Controller
|
||||||
name="seatsPerTimeSlotEnabled"
|
name="seatsPerTimeSlotEnabled"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<>
|
<>
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
@@ -378,7 +372,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
formMethods.setValue("requiresConfirmation", false);
|
formMethods.setValue("requiresConfirmation", false);
|
||||||
setRequiresConfirmation(false);
|
setRequiresConfirmation(false);
|
||||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
formMethods.setValue("seatsPerTimeSlot", 2);
|
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
|
||||||
} else {
|
} else {
|
||||||
formMethods.setValue("seatsPerTimeSlot", null);
|
formMethods.setValue("seatsPerTimeSlot", null);
|
||||||
toggleGuests(true);
|
toggleGuests(true);
|
||||||
@@ -388,8 +382,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||||
<Controller
|
<Controller
|
||||||
name="seatsPerTimeSlot"
|
name="seatsPerTimeSlot"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.seatsPerTimeSlot}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<div>
|
<div>
|
||||||
<TextField
|
<TextField
|
||||||
@@ -399,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
label={t("number_of_seats")}
|
label={t("number_of_seats")}
|
||||||
type="number"
|
type="number"
|
||||||
disabled={seatsLocked.disabled}
|
disabled={seatsLocked.disabled}
|
||||||
defaultValue={value || 2}
|
defaultValue={value}
|
||||||
min={1}
|
min={1}
|
||||||
containerClassName="max-w-80"
|
containerClassName="max-w-80"
|
||||||
addOnSuffix={<>{t("seats")}</>}
|
addOnSuffix={<>{t("seats")}</>}
|
||||||
@@ -408,21 +400,29 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
<div className="mt-4">
|
<div className="mt-4">
|
||||||
<CheckboxField
|
<Controller
|
||||||
description={t("show_attendees")}
|
name="seatsShowAttendees"
|
||||||
disabled={seatsLocked.disabled}
|
render={({ field: { value, onChange } }) => (
|
||||||
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
|
<CheckboxField
|
||||||
defaultChecked={!!eventType.seatsShowAttendees}
|
description={t("show_attendees")}
|
||||||
|
disabled={seatsLocked.disabled}
|
||||||
|
onChange={(e) => onChange(e)}
|
||||||
|
checked={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div className="mt-2">
|
<div className="mt-2">
|
||||||
<CheckboxField
|
<Controller
|
||||||
description={t("show_available_seats_count")}
|
name="seatsShowAvailabilityCount"
|
||||||
disabled={seatsLocked.disabled}
|
render={({ field: { value, onChange } }) => (
|
||||||
onChange={(e) =>
|
<CheckboxField
|
||||||
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
|
description={t("show_available_seats_count")}
|
||||||
}
|
disabled={seatsLocked.disabled}
|
||||||
defaultChecked={!!eventType.seatsShowAvailabilityCount}
|
onChange={(e) => onChange(e)}
|
||||||
|
checked={value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -436,8 +436,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="lockTimeZoneToggleOnBookingPage"
|
name="lockTimeZoneToggleOnBookingPage"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
|
|
||||||
render={({ field: { value, onChange } }) => (
|
render={({ field: { value, onChange } }) => (
|
||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
labelClassName="text-sm"
|
labelClassName="text-sm"
|
||||||
@@ -452,59 +450,47 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
{allowDisablingAttendeeConfirmationEmails(workflows) && (
|
{allowDisablingAttendeeConfirmationEmails(workflows) && (
|
||||||
<>
|
<Controller
|
||||||
<Controller
|
name="metadata.disableStandardEmails.confirmation.attendee"
|
||||||
name="metadata.disableStandardEmails.confirmation.attendee"
|
render={({ field: { value, onChange } }) => (
|
||||||
control={formMethods.control}
|
<>
|
||||||
render={({ field: { value, onChange } }) => (
|
<SettingsToggle
|
||||||
<>
|
labelClassName="text-sm"
|
||||||
<SettingsToggle
|
toggleSwitchAtTheEnd={true}
|
||||||
labelClassName="text-sm"
|
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||||
toggleSwitchAtTheEnd={true}
|
title={t("disable_attendees_confirmation_emails")}
|
||||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
description={t("disable_attendees_confirmation_emails_description")}
|
||||||
title={t("disable_attendees_confirmation_emails")}
|
checked={value}
|
||||||
description={t("disable_attendees_confirmation_emails_description")}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
checked={value || false}
|
/>
|
||||||
onCheckedChange={(e) => {
|
</>
|
||||||
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
|
)}
|
||||||
onChange(e);
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{allowDisablingHostConfirmationEmails(workflows) && (
|
{allowDisablingHostConfirmationEmails(workflows) && (
|
||||||
<>
|
<Controller
|
||||||
<Controller
|
name="metadata.disableStandardEmails.confirmation.host"
|
||||||
name="metadata.disableStandardEmails.confirmation.host"
|
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
|
||||||
control={formMethods.control}
|
render={({ field: { value, onChange } }) => (
|
||||||
defaultValue={!!eventType.seatsPerTimeSlot}
|
<>
|
||||||
render={({ field: { value, onChange } }) => (
|
<SettingsToggle
|
||||||
<>
|
labelClassName="text-sm"
|
||||||
<SettingsToggle
|
toggleSwitchAtTheEnd={true}
|
||||||
labelClassName="text-sm"
|
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
||||||
toggleSwitchAtTheEnd={true}
|
title={t("disable_host_confirmation_emails")}
|
||||||
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
|
description={t("disable_host_confirmation_emails_description")}
|
||||||
title={t("disable_host_confirmation_emails")}
|
checked={value}
|
||||||
description={t("disable_host_confirmation_emails_description")}
|
onCheckedChange={(e) => onChange(e)}
|
||||||
checked={value || false}
|
/>
|
||||||
onCheckedChange={(e) => {
|
</>
|
||||||
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
|
)}
|
||||||
onChange(e);
|
/>
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
</>
|
|
||||||
)}
|
)}
|
||||||
{showEventNameTip && (
|
{showEventNameTip && (
|
||||||
<CustomEventTypeModal
|
<CustomEventTypeModal
|
||||||
close={closeEventNameTip}
|
close={closeEventNameTip}
|
||||||
setValue={setEventName}
|
setValue={(val: string) => formMethods.setValue("eventName", val)}
|
||||||
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
|
defaultValue={formMethods.getValues("eventName")}
|
||||||
placeHolder={eventNamePlaceholder}
|
placeHolder={eventNamePlaceholder}
|
||||||
event={eventNameObject}
|
event={eventNameObject}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -18,21 +18,21 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
|
|||||||
|
|
||||||
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
|
const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||||
extendsFeature: "EventType",
|
extendsFeature: "EventType",
|
||||||
teamId: eventType.team?.id || eventType.parent?.teamId,
|
teamId: eventType.team?.id || eventType.parent?.teamId,
|
||||||
});
|
});
|
||||||
|
|
||||||
const methods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const installedApps =
|
const installedApps =
|
||||||
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
|
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
|
||||||
const notInstalledApps =
|
const notInstalledApps =
|
||||||
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
|
eventTypeApps?.items.filter((app) => !app.userCredentialIds.length && !app.teams.length) || [];
|
||||||
const allAppsData = methods.watch("metadata")?.apps || {};
|
const allAppsData = formMethods.watch("metadata")?.apps || {};
|
||||||
|
|
||||||
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
const setAllAppsData = (_allAppsData: typeof allAppsData) => {
|
||||||
methods.setValue("metadata", {
|
formMethods.setValue("metadata", {
|
||||||
...methods.getValues("metadata"),
|
...formMethods.getValues("metadata"),
|
||||||
apps: _allAppsData,
|
apps: _allAppsData,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventTypeFormMetadata = methods.getValues("metadata");
|
const eventTypeFormMetadata = formMethods.getValues("metadata");
|
||||||
|
|
||||||
const getAppDataSetter = (
|
const getAppDataSetter = (
|
||||||
appId: EventTypeAppsList,
|
appId: EventTypeAppsList,
|
||||||
@@ -56,8 +56,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
): SetAppData => {
|
): SetAppData => {
|
||||||
return function (key, value) {
|
return function (key, value) {
|
||||||
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
// Always get latest data available in Form because consequent calls to setData would update the Form but not allAppsData(it would update during next render)
|
||||||
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
|
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
|
||||||
|
|
||||||
const appData = allAppsDataFromForm[appId];
|
const appData = allAppsDataFromForm[appId];
|
||||||
setAllAppsData({
|
setAllAppsData({
|
||||||
...allAppsDataFromForm,
|
...allAppsDataFromForm,
|
||||||
@@ -138,7 +137,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
message={t("locked_apps_description")}
|
message={t("locked_apps_description")}
|
||||||
/>
|
/>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !installedApps?.length ? (
|
{!isPending && !installedApps?.length ? (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
Icon={Grid}
|
Icon={Grid}
|
||||||
headline={t("empty_installed_apps_headline")}
|
headline={t("empty_installed_apps_headline")}
|
||||||
@@ -179,7 +178,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
|
|||||||
</div>
|
</div>
|
||||||
{!shouldLockDisableProps("apps").disabled && (
|
{!shouldLockDisableProps("apps").disabled && (
|
||||||
<div className="bg-muted mt-6 rounded-md p-8">
|
<div className="bg-muted mt-6 rounded-md p-8">
|
||||||
{!isLoading && notInstalledApps?.length ? (
|
{!isPending && notInstalledApps?.length ? (
|
||||||
<>
|
<>
|
||||||
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
|
<h2 className="text-emphasis mb-2 text-xl font-semibold leading-5 tracking-[0.01em]">
|
||||||
{t("available_apps_lower_case")}
|
{t("available_apps_lower_case")}
|
||||||
|
|||||||
@@ -85,7 +85,7 @@ const EventTypeScheduleDetails = memo(
|
|||||||
const { watch } = useFormContext<FormValues>();
|
const { watch } = useFormContext<FormValues>();
|
||||||
|
|
||||||
const scheduleId = watch("schedule");
|
const scheduleId = watch("schedule");
|
||||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
const { isPending, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{
|
{
|
||||||
scheduleId:
|
scheduleId:
|
||||||
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
scheduleId || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
|
||||||
@@ -112,7 +112,7 @@ const EventTypeScheduleDetails = memo(
|
|||||||
)}>
|
)}>
|
||||||
{day}
|
{day}
|
||||||
</span>
|
</span>
|
||||||
{isLoading ? (
|
{isPending ? (
|
||||||
<SkeletonText className="block h-5 w-60" />
|
<SkeletonText className="block h-5 w-60" />
|
||||||
) : isAvailable ? (
|
) : isAvailable ? (
|
||||||
<div className="space-y-3 text-right">
|
<div className="space-y-3 text-right">
|
||||||
@@ -142,7 +142,7 @@ const EventTypeScheduleDetails = memo(
|
|||||||
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
|
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
|
||||||
<Button
|
<Button
|
||||||
href={`/availability/${schedule.id}`}
|
href={`/availability/${schedule.id}`}
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
color="minimal"
|
color="minimal"
|
||||||
EndIcon={ExternalLink}
|
EndIcon={ExternalLink}
|
||||||
target="_blank"
|
target="_blank"
|
||||||
@@ -169,8 +169,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
const watchSchedule = watch("schedule");
|
const watchSchedule = watch("schedule");
|
||||||
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
const [options, setOptions] = useState<AvailabilityOption[]>([]);
|
||||||
|
|
||||||
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
|
const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
|
||||||
onSuccess: ({ schedules }) => {
|
|
||||||
|
useEffect(
|
||||||
|
function refactorMeWithoutEffect() {
|
||||||
|
if (!data) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const schedules = data.schedules;
|
||||||
|
|
||||||
const options = schedules.map((schedule) => ({
|
const options = schedules.map((schedule) => ({
|
||||||
value: schedule.id,
|
value: schedule.id,
|
||||||
label: schedule.name,
|
label: schedule.name,
|
||||||
@@ -225,8 +232,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
|
|
||||||
setValue("availability", value);
|
setValue("availability", value);
|
||||||
},
|
},
|
||||||
});
|
[data]
|
||||||
|
);
|
||||||
const availabilityValue = watch("availability");
|
const availabilityValue = watch("availability");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -241,8 +248,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
|
|||||||
{t("availability")}
|
{t("availability")}
|
||||||
{shouldLockIndicator("availability")}
|
{shouldLockIndicator("availability")}
|
||||||
</label>
|
</label>
|
||||||
{isLoading && <SelectSkeletonLoader />}
|
{isPending && <SelectSkeletonLoader />}
|
||||||
{!isLoading && (
|
{!isPending && (
|
||||||
<Controller
|
<Controller
|
||||||
name="schedule"
|
name="schedule"
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
import { useAutoAnimate } from "@formkit/auto-animate/react";
|
||||||
import * as RadioGroup from "@radix-ui/react-radio-group";
|
import * as RadioGroup from "@radix-ui/react-radio-group";
|
||||||
import type { EventTypeSetupProps, FormValues } from "pages/event-types/[type]";
|
import type { FormValues } from "pages/event-types/[type]";
|
||||||
import type { Key } from "react";
|
import type { Key } from "react";
|
||||||
import React, { useEffect, useState } from "react";
|
import React, { useEffect, useState } from "react";
|
||||||
import type { UseFormRegisterReturn } from "react-hook-form";
|
import type { UseFormRegisterReturn } from "react-hook-form";
|
||||||
import { Controller, useFormContext, useWatch } from "react-hook-form";
|
import { Controller, useFormContext } from "react-hook-form";
|
||||||
import type { SingleValue } from "react-select";
|
import type { SingleValue } from "react-select";
|
||||||
|
|
||||||
import { classNames } from "@calcom/lib";
|
import { classNames } from "@calcom/lib";
|
||||||
@@ -107,7 +107,7 @@ const MinimumBookingNoticeInput = React.forwardRef<
|
|||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventType">) => {
|
export const EventLimitsTab = () => {
|
||||||
const { t, i18n } = useLocale();
|
const { t, i18n } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
|
||||||
@@ -126,36 +126,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const periodType =
|
const watchPeriodType = formMethods.watch("periodType");
|
||||||
PERIOD_TYPES.find((s) => s.type === eventType.periodType) ||
|
|
||||||
PERIOD_TYPES.find((s) => s.type === "UNLIMITED");
|
|
||||||
|
|
||||||
const [periodDates] = useState<{ startDate: Date; endDate: Date }>({
|
|
||||||
startDate: new Date(eventType.periodStartDate || Date.now()),
|
|
||||||
endDate: new Date(eventType.periodEndDate || Date.now()),
|
|
||||||
});
|
|
||||||
const watchPeriodType = useWatch({
|
|
||||||
control: formMethods.control,
|
|
||||||
name: "periodType",
|
|
||||||
defaultValue: periodType?.type,
|
|
||||||
});
|
|
||||||
|
|
||||||
const optionsPeriod = [
|
const optionsPeriod = [
|
||||||
{ value: 1, label: t("calendar_days") },
|
|
||||||
{ value: 0, label: t("business_days") },
|
{ value: 0, label: t("business_days") },
|
||||||
|
{ value: 1, label: t("calendar_days") },
|
||||||
];
|
];
|
||||||
|
|
||||||
// offsetStart toggle is client-side only, opened by default if offsetStart is set
|
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
|
||||||
const offsetStartValue = useWatch({
|
|
||||||
control: formMethods.control,
|
|
||||||
name: "offsetStart",
|
|
||||||
});
|
|
||||||
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
|
|
||||||
|
|
||||||
// Preview how the offset will affect start times
|
// Preview how the offset will affect start times
|
||||||
|
const watchOffsetStartValue = formMethods.watch("offsetStart");
|
||||||
const offsetOriginalTime = new Date();
|
const offsetOriginalTime = new Date();
|
||||||
offsetOriginalTime.setHours(9, 0, 0, 0);
|
offsetOriginalTime.setHours(9, 0, 0, 0);
|
||||||
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + offsetStartValue * 60 * 1000);
|
const offsetAdjustedTime = new Date(offsetOriginalTime.getTime() + watchOffsetStartValue * 60 * 1000);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
@@ -165,8 +149,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
|
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="beforeBufferTime"
|
name="beforeBufferTime"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.beforeEventBuffer || 0}
|
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const beforeBufferOptions = [
|
const beforeBufferOptions = [
|
||||||
{
|
{
|
||||||
@@ -197,8 +179,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
|
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="afterBufferTime"
|
name="afterBufferTime"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.afterEventBuffer || 0}
|
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
const afterBufferOptions = [
|
const afterBufferOptions = [
|
||||||
{
|
{
|
||||||
@@ -235,7 +215,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
|
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
|
||||||
<Controller
|
<Controller
|
||||||
name="slotInterval"
|
name="slotInterval"
|
||||||
control={formMethods.control}
|
|
||||||
render={() => {
|
render={() => {
|
||||||
const slotIntervalOptions = [
|
const slotIntervalOptions = [
|
||||||
{
|
{
|
||||||
@@ -254,8 +233,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
|
||||||
}}
|
}}
|
||||||
defaultValue={
|
defaultValue={
|
||||||
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
|
slotIntervalOptions.find(
|
||||||
slotIntervalOptions[0]
|
(option) => option.value === formMethods.getValues("slotInterval")
|
||||||
|
) || slotIntervalOptions[0]
|
||||||
}
|
}
|
||||||
options={slotIntervalOptions}
|
options={slotIntervalOptions}
|
||||||
/>
|
/>
|
||||||
@@ -267,7 +247,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
</div>
|
</div>
|
||||||
<Controller
|
<Controller
|
||||||
name="bookingLimits"
|
name="bookingLimits"
|
||||||
control={formMethods.control}
|
|
||||||
render={({ field: { value } }) => {
|
render={({ field: { value } }) => {
|
||||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||||
return (
|
return (
|
||||||
@@ -300,7 +279,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="onlyShowFirstAvailableSlot"
|
name="onlyShowFirstAvailableSlot"
|
||||||
control={formMethods.control}
|
|
||||||
render={({ field: { value } }) => {
|
render={({ field: { value } }) => {
|
||||||
const isChecked = value;
|
const isChecked = value;
|
||||||
return (
|
return (
|
||||||
@@ -323,7 +301,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="durationLimits"
|
name="durationLimits"
|
||||||
control={formMethods.control}
|
|
||||||
render={({ field: { value } }) => {
|
render={({ field: { value } }) => {
|
||||||
const isChecked = Object.keys(value ?? {}).length > 0;
|
const isChecked = Object.keys(value ?? {}).length > 0;
|
||||||
return (
|
return (
|
||||||
@@ -361,7 +338,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
/>
|
/>
|
||||||
<Controller
|
<Controller
|
||||||
name="periodType"
|
name="periodType"
|
||||||
control={formMethods.control}
|
|
||||||
render={({ field: { value } }) => {
|
render={({ field: { value } }) => {
|
||||||
const isChecked = value && value !== "UNLIMITED";
|
const isChecked = value && value !== "UNLIMITED";
|
||||||
|
|
||||||
@@ -380,7 +356,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
|
||||||
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
|
||||||
<RadioGroup.Root
|
<RadioGroup.Root
|
||||||
defaultValue={watchPeriodType}
|
|
||||||
value={watchPeriodType}
|
value={watchPeriodType}
|
||||||
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
|
||||||
{PERIOD_TYPES.map((period) => {
|
{PERIOD_TYPES.map((period) => {
|
||||||
@@ -408,22 +383,23 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
className="border-default my-0 block w-16 text-sm [appearance:textfield] ltr:mr-2 rtl:ml-2"
|
||||||
placeholder="30"
|
placeholder="30"
|
||||||
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
{...formMethods.register("periodDays", { valueAsNumber: true })}
|
||||||
defaultValue={eventType.periodDays || 30}
|
|
||||||
/>
|
/>
|
||||||
<Select
|
<Select
|
||||||
options={optionsPeriod}
|
options={optionsPeriod}
|
||||||
isSearchable={false}
|
isSearchable={false}
|
||||||
onChange={(opt) => {
|
onChange={(opt) =>
|
||||||
formMethods.setValue(
|
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "0")
|
||||||
"periodCountCalendarDays",
|
|
||||||
opt?.value.toString() as "0" | "1"
|
|
||||||
);
|
|
||||||
}}
|
|
||||||
defaultValue={
|
|
||||||
optionsPeriod.find(
|
|
||||||
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
|
|
||||||
) ?? optionsPeriod[0]
|
|
||||||
}
|
}
|
||||||
|
name="periodCoundCalendarDays"
|
||||||
|
value={optionsPeriod.find((opt) => {
|
||||||
|
opt.value ===
|
||||||
|
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0);
|
||||||
|
})}
|
||||||
|
defaultValue={optionsPeriod.find(
|
||||||
|
(opt) =>
|
||||||
|
opt.value ===
|
||||||
|
(formMethods.getValues("periodCountCalendarDays") === "1" ? 1 : 0)
|
||||||
|
)}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
@@ -431,8 +407,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
<div className="me-2 ms-2 inline-flex space-x-2 rtl:space-x-reverse">
|
||||||
<Controller
|
<Controller
|
||||||
name="periodDates"
|
name="periodDates"
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={periodDates}
|
|
||||||
render={() => (
|
render={() => (
|
||||||
<DateRangePicker
|
<DateRangePicker
|
||||||
startDate={formMethods.getValues("periodDates").startDate}
|
startDate={formMethods.getValues("periodDates").startDate}
|
||||||
@@ -481,7 +455,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
|
|||||||
type="number"
|
type="number"
|
||||||
containerClassName="max-w-80"
|
containerClassName="max-w-80"
|
||||||
label={t("offset_start")}
|
label={t("offset_start")}
|
||||||
{...formMethods.register("offsetStart")}
|
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
hint={t("offset_start_description", {
|
hint={t("offset_start_description", {
|
||||||
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
|
||||||
|
|||||||
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
|
|||||||
|
|
||||||
const requirePayment = paymentAppData.price > 0;
|
const requirePayment = paymentAppData.price > 0;
|
||||||
|
|
||||||
return (
|
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
|
||||||
<div className="">
|
|
||||||
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
@@ -48,21 +48,19 @@ const getLocationFromType = (
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
const getLocationInfo = ({
|
||||||
|
eventType,
|
||||||
|
locationOptions,
|
||||||
|
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
|
||||||
const locationAvailable =
|
const locationAvailable =
|
||||||
props.eventType.locations &&
|
eventType.locations &&
|
||||||
props.eventType.locations.length > 0 &&
|
eventType.locations.length > 0 &&
|
||||||
props.locationOptions.some((op) =>
|
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
|
||||||
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
|
const locationDetails = eventType.locations &&
|
||||||
);
|
eventType.locations.length > 0 &&
|
||||||
const locationDetails = props.eventType.locations &&
|
|
||||||
props.eventType.locations.length > 0 &&
|
|
||||||
!locationAvailable && {
|
!locationAvailable && {
|
||||||
slug: props.eventType.locations[0].type
|
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
|
||||||
.replace("integrations:", "")
|
name: eventType.locations[0].type
|
||||||
.replace(":", "-")
|
|
||||||
.replace("_video", ""),
|
|
||||||
name: props.eventType.locations[0].type
|
|
||||||
.replace("integrations:", "")
|
.replace("integrations:", "")
|
||||||
.replace(":", " ")
|
.replace(":", " ")
|
||||||
.replace("_video", "")
|
.replace("_video", "")
|
||||||
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
|
|||||||
};
|
};
|
||||||
return { locationAvailable, locationDetails };
|
return { locationAvailable, locationDetails };
|
||||||
};
|
};
|
||||||
interface DescriptionEditorProps {
|
|
||||||
description?: string | null;
|
|
||||||
editable?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
const DescriptionEditor = (props: DescriptionEditorProps) => {
|
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const [mounted, setIsMounted] = useState(false);
|
const [mounted, setIsMounted] = useState(false);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { description } = props;
|
|
||||||
const [firstRender, setFirstRender] = useState(true);
|
const [firstRender, setFirstRender] = useState(true);
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
setIsMounted(true);
|
setIsMounted(true);
|
||||||
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
|
|||||||
|
|
||||||
return mounted ? (
|
return mounted ? (
|
||||||
<Editor
|
<Editor
|
||||||
getText={() => md.render(formMethods.getValues("description") || description || "")}
|
getText={() => md.render(formMethods.getValues("description") || "")}
|
||||||
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
setText={(value: string) => formMethods.setValue("description", turndown(value))}
|
||||||
excludedToolbarItems={["blockType"]}
|
excludedToolbarItems={["blockType"]}
|
||||||
placeholder={t("quick_video_meeting")}
|
placeholder={t("quick_video_meeting")}
|
||||||
editable={props.editable}
|
editable={isEditable}
|
||||||
firstRender={firstRender}
|
firstRender={firstRender}
|
||||||
setFirstRender={setFirstRender}
|
setFirstRender={setFirstRender}
|
||||||
/>
|
/>
|
||||||
@@ -113,7 +106,9 @@ export const EventSetupTab = (
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
const { eventType, team, destinationCalendar } = props;
|
const { eventType, team, destinationCalendar } = props;
|
||||||
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
|
const [multipleDuration, setMultipleDuration] = useState(
|
||||||
|
formMethods.getValues("metadata")?.multipleDuration
|
||||||
|
);
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
|
||||||
|
|
||||||
@@ -143,12 +138,12 @@ export const EventSetupTab = (
|
|||||||
}>
|
}>
|
||||||
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
|
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
|
||||||
const [defaultDuration, setDefaultDuration] = useState(
|
const [defaultDuration, setDefaultDuration] = useState(
|
||||||
selectedMultipleDuration.find((opt) => opt.value === eventType.length) ?? null
|
selectedMultipleDuration.find((opt) => opt.value === formMethods.getValues("length")) ?? null
|
||||||
);
|
);
|
||||||
|
|
||||||
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
const { isChildrenManagedEventType, isManagedEventType, shouldLockIndicator, shouldLockDisableProps } =
|
||||||
useLockedFieldsManager(
|
useLockedFieldsManager(
|
||||||
eventType,
|
formMethods.getValues(),
|
||||||
t("locked_fields_admin_description"),
|
t("locked_fields_admin_description"),
|
||||||
t("locked_fields_member_description")
|
t("locked_fields_member_description")
|
||||||
);
|
);
|
||||||
@@ -195,7 +190,6 @@ export const EventSetupTab = (
|
|||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
@@ -219,7 +213,6 @@ export const EventSetupTab = (
|
|||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={defaultValue}
|
defaultValue={defaultValue}
|
||||||
render={({ field: { onChange, value } }) => {
|
render={({ field: { onChange, value } }) => {
|
||||||
return (
|
return (
|
||||||
@@ -339,7 +332,7 @@ export const EventSetupTab = (
|
|||||||
defaultChecked={defaultLocation?.displayLocationPublicly}
|
defaultChecked={defaultLocation?.displayLocationPublicly}
|
||||||
description={t("display_location_label")}
|
description={t("display_location_label")}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const fieldValues = formMethods.getValues().locations[index];
|
const fieldValues = formMethods.getValues("locations")[index];
|
||||||
updateLocationField(index, {
|
updateLocationField(index, {
|
||||||
...fieldValues,
|
...fieldValues,
|
||||||
displayLocationPublicly: e.target.checked,
|
displayLocationPublicly: e.target.checked,
|
||||||
@@ -408,7 +401,7 @@ export const EventSetupTab = (
|
|||||||
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
The “Add to calendar” for this event type needs to be a Google Calendar for Meet to work.
|
||||||
Change it{" "}
|
Change it{" "}
|
||||||
<Link
|
<Link
|
||||||
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
|
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
|
||||||
className="underline">
|
className="underline">
|
||||||
here.
|
here.
|
||||||
</Link>{" "}
|
</Link>{" "}
|
||||||
@@ -463,7 +456,6 @@ export const EventSetupTab = (
|
|||||||
required
|
required
|
||||||
label={t("title")}
|
label={t("title")}
|
||||||
{...shouldLockDisableProps("title")}
|
{...shouldLockDisableProps("title")}
|
||||||
defaultValue={eventType.title}
|
|
||||||
{...formMethods.register("title")}
|
{...formMethods.register("title")}
|
||||||
/>
|
/>
|
||||||
<div>
|
<div>
|
||||||
@@ -471,23 +463,19 @@ export const EventSetupTab = (
|
|||||||
{t("description")}
|
{t("description")}
|
||||||
{shouldLockIndicator("description")}
|
{shouldLockIndicator("description")}
|
||||||
</Label>
|
</Label>
|
||||||
<DescriptionEditor
|
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
|
||||||
description={eventType?.description}
|
|
||||||
editable={!descriptionLockedProps.disabled}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
<TextField
|
<TextField
|
||||||
required
|
required
|
||||||
label={t("URL")}
|
label={t("URL")}
|
||||||
{...shouldLockDisableProps("slug")}
|
{...shouldLockDisableProps("slug")}
|
||||||
defaultValue={eventType.slug}
|
|
||||||
addOnLeading={
|
addOnLeading={
|
||||||
<>
|
<>
|
||||||
{urlPrefix}/
|
{urlPrefix}/
|
||||||
{!isManagedEventType
|
{!isManagedEventType
|
||||||
? team
|
? team
|
||||||
? (orgBranding ? "" : "team/") + team.slug
|
? (orgBranding ? "" : "team/") + team.slug
|
||||||
: eventType.users[0].username
|
: formMethods.getValues("users")[0].username
|
||||||
: t("username_placeholder")}
|
: t("username_placeholder")}
|
||||||
/
|
/
|
||||||
</>
|
</>
|
||||||
@@ -564,7 +552,7 @@ export const EventSetupTab = (
|
|||||||
type="number"
|
type="number"
|
||||||
{...lengthLockedProps}
|
{...lengthLockedProps}
|
||||||
label={t("duration")}
|
label={t("duration")}
|
||||||
defaultValue={eventType.length ?? 15}
|
defaultValue={formMethods.getValues("length") ?? 15}
|
||||||
{...formMethods.register("length")}
|
{...formMethods.register("length")}
|
||||||
addOnSuffix={<>{t("minutes")}</>}
|
addOnSuffix={<>{t("minutes")}</>}
|
||||||
min={1}
|
min={1}
|
||||||
@@ -580,6 +568,8 @@ export const EventSetupTab = (
|
|||||||
onCheckedChange={() => {
|
onCheckedChange={() => {
|
||||||
if (multipleDuration !== undefined) {
|
if (multipleDuration !== undefined) {
|
||||||
setMultipleDuration(undefined);
|
setMultipleDuration(undefined);
|
||||||
|
setSelectedMultipleDuration([]);
|
||||||
|
setDefaultDuration(null);
|
||||||
formMethods.setValue("metadata.multipleDuration", undefined);
|
formMethods.setValue("metadata.multipleDuration", undefined);
|
||||||
formMethods.setValue("length", eventType.length);
|
formMethods.setValue("length", eventType.length);
|
||||||
} else {
|
} else {
|
||||||
@@ -600,12 +590,7 @@ export const EventSetupTab = (
|
|||||||
{shouldLockIndicator("locations")}
|
{shouldLockIndicator("locations")}
|
||||||
</Skeleton>
|
</Skeleton>
|
||||||
|
|
||||||
<Controller
|
<Controller name="locations" render={() => <Locations />} />
|
||||||
name="locations"
|
|
||||||
control={formMethods.control}
|
|
||||||
defaultValue={eventType.locations || []}
|
|
||||||
render={() => <Locations />}
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -67,55 +67,66 @@ type Props = {
|
|||||||
availability?: AvailabilityOption;
|
availability?: AvailabilityOption;
|
||||||
isUserOrganizationAdmin: boolean;
|
isUserOrganizationAdmin: boolean;
|
||||||
bookerUrl: string;
|
bookerUrl: string;
|
||||||
|
activeWebhooksNumber: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
function getNavigation(props: {
|
type getNavigationProps = {
|
||||||
t: TFunction;
|
t: TFunction;
|
||||||
eventType: Props["eventType"];
|
length: number;
|
||||||
|
id: number;
|
||||||
|
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
|
||||||
enabledAppsNumber: number;
|
enabledAppsNumber: number;
|
||||||
enabledWorkflowsNumber: number;
|
enabledWorkflowsNumber: number;
|
||||||
installedAppsNumber: number;
|
installedAppsNumber: number;
|
||||||
availability: AvailabilityOption | undefined;
|
availability: AvailabilityOption | undefined;
|
||||||
}) {
|
};
|
||||||
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
|
|
||||||
const duration =
|
function getNavigation({
|
||||||
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
|
length,
|
||||||
|
id,
|
||||||
|
multipleDuration,
|
||||||
|
t,
|
||||||
|
enabledAppsNumber,
|
||||||
|
installedAppsNumber,
|
||||||
|
enabledWorkflowsNumber,
|
||||||
|
}: getNavigationProps) {
|
||||||
|
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
{
|
{
|
||||||
name: "event_setup_tab_title",
|
name: "event_setup_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=setup`,
|
href: `/event-types/${id}?tabName=setup`,
|
||||||
icon: LinkIcon,
|
icon: LinkIcon,
|
||||||
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_limit_tab_title",
|
name: "event_limit_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=limits`,
|
href: `/event-types/${id}?tabName=limits`,
|
||||||
icon: Clock,
|
icon: Clock,
|
||||||
info: `event_limit_tab_description`,
|
info: `event_limit_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "event_advanced_tab_title",
|
name: "event_advanced_tab_title",
|
||||||
href: `/event-types/${eventType.id}?tabName=advanced`,
|
href: `/event-types/${id}?tabName=advanced`,
|
||||||
icon: Sliders,
|
icon: Sliders,
|
||||||
info: `event_advanced_tab_description`,
|
info: `event_advanced_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "recurring",
|
name: "recurring",
|
||||||
href: `/event-types/${eventType.id}?tabName=recurring`,
|
href: `/event-types/${id}?tabName=recurring`,
|
||||||
icon: Repeat,
|
icon: Repeat,
|
||||||
info: `recurring_event_tab_description`,
|
info: `recurring_event_tab_description`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "apps",
|
name: "apps",
|
||||||
href: `/event-types/${eventType.id}?tabName=apps`,
|
href: `/event-types/${id}?tabName=apps`,
|
||||||
icon: Grid,
|
icon: Grid,
|
||||||
//TODO: Handle proper translation with count handling
|
//TODO: Handle proper translation with count handling
|
||||||
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "workflows",
|
name: "workflows",
|
||||||
href: `/event-types/${eventType.id}?tabName=workflows`,
|
href: `/event-types/${id}?tabName=workflows`,
|
||||||
icon: Zap,
|
icon: Zap,
|
||||||
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
info: `${enabledWorkflowsNumber} ${t("active")}`,
|
||||||
},
|
},
|
||||||
@@ -136,6 +147,7 @@ function EventTypeSingleLayout({
|
|||||||
availability,
|
availability,
|
||||||
isUserOrganizationAdmin,
|
isUserOrganizationAdmin,
|
||||||
bookerUrl,
|
bookerUrl,
|
||||||
|
activeWebhooksNumber,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -145,7 +157,7 @@ function EventTypeSingleLayout({
|
|||||||
const hasPermsToDelete =
|
const hasPermsToDelete =
|
||||||
currentUserMembership?.role !== "MEMBER" ||
|
currentUserMembership?.role !== "MEMBER" ||
|
||||||
!currentUserMembership ||
|
!currentUserMembership ||
|
||||||
eventType.schedulingType === SchedulingType.MANAGED ||
|
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
|
||||||
isUserOrganizationAdmin;
|
isUserOrganizationAdmin;
|
||||||
|
|
||||||
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
|
||||||
@@ -167,16 +179,23 @@ function EventTypeSingleLayout({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
|
||||||
eventType,
|
formMethods.getValues(),
|
||||||
t("locked_fields_admin_description"),
|
t("locked_fields_admin_description"),
|
||||||
t("locked_fields_member_description")
|
t("locked_fields_member_description")
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const length = formMethods.watch("length");
|
||||||
|
const multipleDuration = formMethods.watch("metadata")?.multipleDuration;
|
||||||
|
|
||||||
|
const watchSchedulingType = formMethods.watch("schedulingType");
|
||||||
|
const watchChildrenCount = formMethods.watch("children").length;
|
||||||
// Define tab navigation here
|
// Define tab navigation here
|
||||||
const EventTypeTabs = useMemo(() => {
|
const EventTypeTabs = useMemo(() => {
|
||||||
const navigation = getNavigation({
|
const navigation = getNavigation({
|
||||||
t,
|
t,
|
||||||
eventType,
|
length,
|
||||||
|
multipleDuration,
|
||||||
|
id: formMethods.getValues("id"),
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
@@ -185,31 +204,29 @@ function EventTypeSingleLayout({
|
|||||||
|
|
||||||
navigation.splice(1, 0, {
|
navigation.splice(1, 0, {
|
||||||
name: "availability",
|
name: "availability",
|
||||||
href: `/event-types/${eventType.id}?tabName=availability`,
|
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
|
||||||
icon: Calendar,
|
icon: Calendar,
|
||||||
info:
|
info:
|
||||||
isManagedEventType || isChildrenManagedEventType
|
isManagedEventType || isChildrenManagedEventType
|
||||||
? eventType.schedule === null
|
? formMethods.getValues("schedule") === null
|
||||||
? "members_default_schedule"
|
? "members_default_schedule"
|
||||||
: isChildrenManagedEventType
|
: isChildrenManagedEventType
|
||||||
? `${
|
? `${
|
||||||
eventType.scheduleName
|
formMethods.getValues("scheduleName")
|
||||||
? `${eventType.scheduleName} - ${t("managed")}`
|
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
|
||||||
: `default_schedule_name`
|
: `default_schedule_name`
|
||||||
}`
|
}`
|
||||||
: eventType.scheduleName ?? `default_schedule_name`
|
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
|
||||||
: eventType.scheduleName ?? `default_schedule_name`,
|
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
|
||||||
});
|
});
|
||||||
// If there is a team put this navigation item within the tabs
|
// If there is a team put this navigation item within the tabs
|
||||||
if (team) {
|
if (team) {
|
||||||
navigation.splice(2, 0, {
|
navigation.splice(2, 0, {
|
||||||
name: "assignment",
|
name: "assignment",
|
||||||
href: `/event-types/${eventType.id}?tabName=team`,
|
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
|
||||||
icon: Users,
|
icon: Users,
|
||||||
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
|
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
|
||||||
isManagedEventType
|
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
|
||||||
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
|
|
||||||
: ""
|
|
||||||
}`,
|
}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -225,15 +242,14 @@ function EventTypeSingleLayout({
|
|||||||
}
|
}
|
||||||
navigation.push({
|
navigation.push({
|
||||||
name: "webhooks",
|
name: "webhooks",
|
||||||
href: `/event-types/${eventType.id}?tabName=webhooks`,
|
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
|
||||||
icon: TbWebhook,
|
icon: TbWebhook,
|
||||||
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
|
info: `${activeWebhooksNumber} ${t("active")}`,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return navigation;
|
return navigation;
|
||||||
}, [
|
}, [
|
||||||
t,
|
t,
|
||||||
eventType,
|
|
||||||
enabledAppsNumber,
|
enabledAppsNumber,
|
||||||
installedAppsNumber,
|
installedAppsNumber,
|
||||||
enabledWorkflowsNumber,
|
enabledWorkflowsNumber,
|
||||||
@@ -241,16 +257,23 @@ function EventTypeSingleLayout({
|
|||||||
isManagedEventType,
|
isManagedEventType,
|
||||||
isChildrenManagedEventType,
|
isChildrenManagedEventType,
|
||||||
team,
|
team,
|
||||||
formMethods,
|
length,
|
||||||
|
multipleDuration,
|
||||||
|
formMethods.getValues("id"),
|
||||||
|
watchSchedulingType,
|
||||||
|
watchChildrenCount,
|
||||||
|
activeWebhooksNumber,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
const permalink = `${bookerUrl}/${
|
const permalink = `${bookerUrl}/${
|
||||||
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : eventType.users[0].username
|
team ? `${!team.parentId ? "team/" : ""}${team.slug}` : formMethods.getValues("users")[0].username
|
||||||
}/${eventType.slug}`;
|
}/${eventType.slug}`;
|
||||||
|
|
||||||
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
|
const embedLink = `${
|
||||||
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
|
team ? `team/${team.slug}` : formMethods.getValues("users")[0].username
|
||||||
|
}/${formMethods.getValues("slug")}`;
|
||||||
|
const isManagedEvent = formMethods.getValues("schedulingType") === SchedulingType.MANAGED ? "_managed" : "";
|
||||||
|
// const title = formMethods.watch("title");
|
||||||
return (
|
return (
|
||||||
<Shell
|
<Shell
|
||||||
backPath="/event-types"
|
backPath="/event-types"
|
||||||
@@ -258,7 +281,7 @@ function EventTypeSingleLayout({
|
|||||||
heading={eventType.title}
|
heading={eventType.title}
|
||||||
CTA={
|
CTA={
|
||||||
<div className="flex items-center justify-end">
|
<div className="flex items-center justify-end">
|
||||||
{!eventType.metadata?.managedEventConfig && (
|
{!formMethods.getValues("metadata")?.managedEventConfig && (
|
||||||
<>
|
<>
|
||||||
<div
|
<div
|
||||||
className={classNames(
|
className={classNames(
|
||||||
@@ -333,7 +356,7 @@ function EventTypeSingleLayout({
|
|||||||
tooltip={t("embed")}
|
tooltip={t("embed")}
|
||||||
tooltipSide="bottom"
|
tooltipSide="bottom"
|
||||||
tooltipOffset={4}
|
tooltipOffset={4}
|
||||||
eventId={eventType.id}
|
eventId={formMethods.getValues("id")}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
@@ -446,14 +469,14 @@ function EventTypeSingleLayout({
|
|||||||
</Suspense>
|
</Suspense>
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isPending={deleteMutation.isPending}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t(`delete${isManagedEvent}_event_type`)}
|
title={t(`delete${isManagedEvent}_event_type`)}
|
||||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||||
loadingText={t(`confirm_delete_event_type`)}
|
loadingText={t(`confirm_delete_event_type`)}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteMutation.mutate({ id: eventType.id });
|
deleteMutation.mutate({ id: formMethods.getValues("id") });
|
||||||
}}>
|
}}>
|
||||||
<p className="mt-5">
|
<p className="mt-5">
|
||||||
<Trans
|
<Trans
|
||||||
|
|||||||
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
|||||||
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
setEditModalOpen(false);
|
setEditModalOpen(false);
|
||||||
await utils.viewer.webhook.list.invalidate();
|
|
||||||
showToast(t("webhook_updated_successfully"), "success");
|
showToast(t("webhook_updated_successfully"), "success");
|
||||||
|
await utils.viewer.webhook.list.invalidate();
|
||||||
|
await utils.viewer.eventTypes.get.invalidate();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
showToast(`${error.message}`, "error");
|
showToast(`${error.message}`, "error");
|
||||||
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
|
|||||||
|
|
||||||
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
|
||||||
async onSuccess() {
|
async onSuccess() {
|
||||||
|
setCreateModalOpen(false);
|
||||||
showToast(t("webhook_created_successfully"), "success");
|
showToast(t("webhook_created_successfully"), "success");
|
||||||
await utils.viewer.webhook.list.invalidate();
|
await utils.viewer.webhook.list.invalidate();
|
||||||
setCreateModalOpen(false);
|
await utils.viewer.eventTypes.get.invalidate();
|
||||||
},
|
},
|
||||||
onError(error) {
|
onError(error) {
|
||||||
showToast(`${error.message}`, "error");
|
showToast(`${error.message}`, "error");
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
|||||||
eventTypeId: eventType.id,
|
eventTypeId: eventType.id,
|
||||||
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
|
||||||
});
|
});
|
||||||
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
|
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||||
variant: "other",
|
variant: "other",
|
||||||
onlyInstalled: true,
|
onlyInstalled: true,
|
||||||
});
|
});
|
||||||
@@ -188,7 +188,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
{webhooks && !isLoading && (
|
{webhooks && !isPending && (
|
||||||
<>
|
<>
|
||||||
<div>
|
<div>
|
||||||
{webhooks.length ? (
|
{webhooks.length ? (
|
||||||
|
|||||||
@@ -8,18 +8,18 @@ interface ManagedEventDialogProps {
|
|||||||
slugExistsChildrenDialogOpen: ChildrenEventType[];
|
slugExistsChildrenDialogOpen: ChildrenEventType[];
|
||||||
slug: string;
|
slug: string;
|
||||||
onOpenChange: () => void;
|
onOpenChange: () => void;
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
onConfirm: (e: { preventDefault: () => void }) => void;
|
onConfirm: (e: { preventDefault: () => void }) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
|
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
|
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
|
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={isLoading}
|
isPending={isPending}
|
||||||
variety="warning"
|
variety="warning"
|
||||||
title={t("managed_event_dialog_title", {
|
title={t("managed_event_dialog_title", {
|
||||||
slug,
|
slug,
|
||||||
|
|||||||
@@ -19,11 +19,10 @@ export default function RecurringEventController({
|
|||||||
paymentEnabled,
|
paymentEnabled,
|
||||||
}: RecurringEventControllerProps) {
|
}: RecurringEventControllerProps) {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
|
||||||
eventType.recurringEvent
|
|
||||||
);
|
|
||||||
const formMethods = useFormContext<FormValues>();
|
const formMethods = useFormContext<FormValues>();
|
||||||
|
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
|
||||||
|
formMethods.getValues("recurringEvent")
|
||||||
|
);
|
||||||
/* Just yearly-0, monthly-1 and weekly-2 */
|
/* Just yearly-0, monthly-1 and weekly-2 */
|
||||||
const recurringEventFreqOptions = Object.entries(Frequency)
|
const recurringEventFreqOptions = Object.entries(Frequency)
|
||||||
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
|
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
|
|||||||
color="secondary"
|
color="secondary"
|
||||||
disabled={installed}
|
disabled={installed}
|
||||||
type="button"
|
type="button"
|
||||||
loading={buttonProps?.isLoading}
|
loading={buttonProps?.isPending}
|
||||||
onClick={(event) => {
|
onClick={(event) => {
|
||||||
// Save cookie key to return url step
|
// Save cookie key to return url step
|
||||||
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{/* Already connected calendars */}
|
{/* Already connected calendars */}
|
||||||
{!queryConnectedCalendars.isLoading &&
|
{!queryConnectedCalendars.isPending &&
|
||||||
firstCalendar &&
|
firstCalendar &&
|
||||||
firstCalendar.integration &&
|
firstCalendar.integration &&
|
||||||
firstCalendar.integration.title &&
|
firstCalendar.integration.title &&
|
||||||
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{queryIntegrations.isLoading && <StepConnectionLoader />}
|
{queryIntegrations.isPending && <StepConnectionLoader />}
|
||||||
|
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface ConnectedAppStepProps {
|
|||||||
|
|
||||||
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
||||||
const { nextStep } = props;
|
const { nextStep } = props;
|
||||||
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
|
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
|
||||||
variant: "conferencing",
|
variant: "conferencing",
|
||||||
onlyInstalled: false,
|
onlyInstalled: false,
|
||||||
sortByMostPopular: true,
|
sortByMostPopular: true,
|
||||||
@@ -26,7 +26,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{!isLoading && (
|
{!isPending && (
|
||||||
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
<List className="bg-default border-subtle divide-subtle scroll-bar mx-1 max-h-[45vh] divide-y !overflow-y-scroll rounded-md border p-0 sm:mx-0">
|
||||||
{queryConnectedVideoApps?.items &&
|
{queryConnectedVideoApps?.items &&
|
||||||
queryConnectedVideoApps?.items.map((item) => {
|
queryConnectedVideoApps?.items.map((item) => {
|
||||||
@@ -48,7 +48,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
|
|||||||
</List>
|
</List>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{isLoading && <StepConnectionLoader />}
|
{isPending && <StepConnectionLoader />}
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
data-testid="save-video-button"
|
data-testid="save-video-button"
|
||||||
|
|||||||
@@ -113,7 +113,7 @@ const UserSettings = (props: IUserSettingsProps) => {
|
|||||||
<Button
|
<Button
|
||||||
type="submit"
|
type="submit"
|
||||||
className="mt-8 flex w-full flex-row justify-center"
|
className="mt-8 flex w-full flex-row justify-center"
|
||||||
disabled={mutation.isLoading}>
|
disabled={mutation.isPending}>
|
||||||
{t("next_step_text")}
|
{t("next_step_text")}
|
||||||
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { Check } from "@calcom/ui/components/icon";
|
|||||||
const StepDone = (props: {
|
const StepDone = (props: {
|
||||||
currentStep: number;
|
currentStep: number;
|
||||||
nextStepPath: string;
|
nextStepPath: string;
|
||||||
setIsLoading: Dispatch<SetStateAction<boolean>>;
|
setIsPending: Dispatch<SetStateAction<boolean>>;
|
||||||
}) => {
|
}) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -18,7 +18,7 @@ const StepDone = (props: {
|
|||||||
name={`wizard-step-${props.currentStep}`}
|
name={`wizard-step-${props.currentStep}`}
|
||||||
className="flex justify-center space-y-4"
|
className="flex justify-center space-y-4"
|
||||||
onSubmit={(e) => {
|
onSubmit={(e) => {
|
||||||
props.setIsLoading(true);
|
props.setIsPending(true);
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
router.replace(props.nextStepPath);
|
router.replace(props.nextStepPath);
|
||||||
}}>
|
}}>
|
||||||
|
|||||||
@@ -293,7 +293,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isPending}
|
||||||
data-testid="go-to-billing"
|
data-testid="go-to-billing"
|
||||||
href={paymentLink}>
|
href={paymentLink}>
|
||||||
<>
|
<>
|
||||||
@@ -305,7 +305,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
|
|||||||
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
|
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsername.isLoading}
|
loading={updateUsername.isPending}
|
||||||
data-testid="save-username"
|
data-testid="save-username"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
saveUsername();
|
saveUsername();
|
||||||
|
|||||||
@@ -177,7 +177,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
|
|||||||
<DialogFooter className="mt-4">
|
<DialogFooter className="mt-4">
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
loading={updateUsernameMutation.isLoading}
|
loading={updateUsernameMutation.isPending}
|
||||||
data-testid="save-username"
|
data-testid="save-username"
|
||||||
onClick={updateUsername}>
|
onClick={updateUsername}>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
|
|||||||
@@ -2,8 +2,7 @@ import type { InputHTMLAttributes } from "react";
|
|||||||
import React, { forwardRef } from "react";
|
import React, { forwardRef } from "react";
|
||||||
|
|
||||||
import classNames from "@calcom/lib/classNames";
|
import classNames from "@calcom/lib/classNames";
|
||||||
|
import { InfoBadge } from "@calcom/ui";
|
||||||
import InfoBadge from "@components/ui/InfoBadge";
|
|
||||||
|
|
||||||
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
type Props = InputHTMLAttributes<HTMLInputElement> & {
|
||||||
label?: React.ReactNode;
|
label?: React.ReactNode;
|
||||||
|
|||||||
@@ -1 +0,0 @@
|
|||||||
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
|
|
||||||
@@ -1,21 +1,15 @@
|
|||||||
import type {
|
import type {
|
||||||
QueryObserverLoadingErrorResult,
|
QueryObserverPendingResult,
|
||||||
QueryObserverLoadingResult,
|
|
||||||
QueryObserverRefetchErrorResult,
|
QueryObserverRefetchErrorResult,
|
||||||
QueryObserverSuccessResult,
|
QueryObserverSuccessResult,
|
||||||
|
QueryObserverLoadingErrorResult,
|
||||||
UseQueryResult,
|
UseQueryResult,
|
||||||
} from "@tanstack/react-query";
|
} from "@tanstack/react-query";
|
||||||
import type { ReactNode } from "react";
|
import type { ReactNode } from "react";
|
||||||
|
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import type { TRPCClientErrorLike } from "@calcom/trpc/client";
|
|
||||||
import type { DecorateProcedure } from "@calcom/trpc/react/shared";
|
|
||||||
import type { AnyQueryProcedure, inferProcedureInput, inferProcedureOutput } from "@calcom/trpc/server";
|
|
||||||
import type { AppRouter } from "@calcom/trpc/server/routers/_app";
|
|
||||||
import { Alert, Loader } from "@calcom/ui";
|
import { Alert, Loader } from "@calcom/ui";
|
||||||
|
|
||||||
import type { UseTRPCQueryOptions } from "@trpc/react-query/shared";
|
|
||||||
|
|
||||||
type ErrorLike = {
|
type ErrorLike = {
|
||||||
message: string;
|
message: string;
|
||||||
};
|
};
|
||||||
@@ -27,7 +21,7 @@ interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
|
|||||||
error?: (
|
error?: (
|
||||||
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
|
||||||
) => JSXElementOrNull;
|
) => JSXElementOrNull;
|
||||||
loading?: (query: QueryObserverLoadingResult<TData, TError> | null) => JSXElementOrNull;
|
loading?: (query: QueryObserverPendingResult<TData, TError> | null) => JSXElementOrNull;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
|
||||||
@@ -58,8 +52,11 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
|||||||
const { isLocaleReady } = useLocale();
|
const { isLocaleReady } = useLocale();
|
||||||
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
|
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
|
||||||
|
|
||||||
if (query.status === "loading" || !isLocaleReady) {
|
if (!isLocaleReady) {
|
||||||
return opts.loading?.(query.status === "loading" ? query : null) ?? StatusLoader;
|
return opts.loading?.(query.status === "pending" ? query : null) ?? StatusLoader;
|
||||||
|
}
|
||||||
|
if (query.status === "pending") {
|
||||||
|
return opts.loading?.(query) ?? StatusLoader;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (query.status === "success") {
|
if (query.status === "success") {
|
||||||
@@ -81,28 +78,3 @@ export function QueryCell<TData, TError extends ErrorLike>(
|
|||||||
// impossible state
|
// impossible state
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
type TError = TRPCClientErrorLike<AppRouter>;
|
|
||||||
|
|
||||||
const withQuery = <
|
|
||||||
TQuery extends AnyQueryProcedure,
|
|
||||||
TInput = inferProcedureInput<TQuery>,
|
|
||||||
TOutput = inferProcedureOutput<TQuery>
|
|
||||||
>(
|
|
||||||
queryProcedure: DecorateProcedure<TQuery, inferProcedureInput<TQuery>, inferProcedureOutput<TQuery>>,
|
|
||||||
|
|
||||||
input?: TInput,
|
|
||||||
params?: UseTRPCQueryOptions<TQuery, TInput, TOutput, TOutput, TError>
|
|
||||||
) => {
|
|
||||||
return function WithQuery(
|
|
||||||
opts: Omit<
|
|
||||||
Partial<QueryCellOptionsWithEmpty<TOutput, TError>> & QueryCellOptionsNoEmpty<TOutput, TError>,
|
|
||||||
"query"
|
|
||||||
>
|
|
||||||
) {
|
|
||||||
const query = queryProcedure.useQuery(input, params);
|
|
||||||
return <QueryCell query={query} {...opts} />;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
|
|
||||||
export { withQuery };
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "@calcom/web",
|
"name": "@calcom/web",
|
||||||
"version": "3.7.3",
|
"version": "3.7.8",
|
||||||
"private": true,
|
"private": true,
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"analyze": "ANALYZE=true next build",
|
"analyze": "ANALYZE=true next build",
|
||||||
@@ -60,7 +60,7 @@
|
|||||||
"@sentry/nextjs": "^7.73.0",
|
"@sentry/nextjs": "^7.73.0",
|
||||||
"@stripe/react-stripe-js": "^1.10.0",
|
"@stripe/react-stripe-js": "^1.10.0",
|
||||||
"@stripe/stripe-js": "^1.35.0",
|
"@stripe/stripe-js": "^1.35.0",
|
||||||
"@tanstack/react-query": "^4.3.9",
|
"@tanstack/react-query": "^5.17.15",
|
||||||
"@tremor/react": "^2.0.0",
|
"@tremor/react": "^2.0.0",
|
||||||
"@types/turndown": "^5.0.1",
|
"@types/turndown": "^5.0.1",
|
||||||
"@upstash/ratelimit": "^0.4.3",
|
"@upstash/ratelimit": "^0.4.3",
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import jwt from "jsonwebtoken";
|
import jwt from "jsonwebtoken";
|
||||||
import type { NextApiRequest, NextApiResponse } from "next";
|
import type { NextApiRequest, NextApiResponse } from "next";
|
||||||
import type { OAuthTokenPayload } from "pages/api/auth/oauth/token";
|
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||||
|
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
|
|||||||
@@ -3,14 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
|
|||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
|
||||||
|
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||||
export type OAuthTokenPayload = {
|
|
||||||
userId?: number | null;
|
|
||||||
teamId?: number | null;
|
|
||||||
token_type: string;
|
|
||||||
scope: string[];
|
|
||||||
clientId: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
|
||||||
if (req.method !== "POST") {
|
if (req.method !== "POST") {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import classNames from "classnames";
|
|||||||
import { signIn } from "next-auth/react";
|
import { signIn } from "next-auth/react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState, useEffect } from "react";
|
||||||
import { FormProvider, useForm } from "react-hook-form";
|
import { FormProvider, useForm } from "react-hook-form";
|
||||||
import { FaGoogle } from "react-icons/fa";
|
import { FaGoogle } from "react-icons/fa";
|
||||||
import { z } from "zod";
|
import { z } from "zod";
|
||||||
@@ -157,15 +157,20 @@ inferSSRProps<typeof getServerSideProps> & WithNonceProps<{}>) {
|
|||||||
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
|
||||||
};
|
};
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.public.ssoConnections.useQuery(undefined, {
|
const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery();
|
||||||
onError: (err) => {
|
|
||||||
setErrorMessage(err.message);
|
useEffect(
|
||||||
|
function refactorMeWithoutEffect() {
|
||||||
|
if (error) {
|
||||||
|
setErrorMessage(error.message);
|
||||||
|
}
|
||||||
},
|
},
|
||||||
});
|
[error]
|
||||||
|
);
|
||||||
|
|
||||||
const displaySSOLogin = HOSTED_CAL_FEATURES
|
const displaySSOLogin = HOSTED_CAL_FEATURES
|
||||||
? true
|
? true
|
||||||
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
|
: isSAMLLoginEnabled && !isPending && data?.connectionExists;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
<div className="dark:bg-brand dark:text-brand-contrast text-emphasis min-h-screen [--cal-brand-emphasis:#101010] [--cal-brand-subtle:#9CA3AF] [--cal-brand-text:white] [--cal-brand:#111827] dark:[--cal-brand-emphasis:#e1e1e1] dark:[--cal-brand-text:black] dark:[--cal-brand:white]">
|
||||||
|
|||||||
@@ -28,7 +28,7 @@ export default function Authorize() {
|
|||||||
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
|
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
|
||||||
const scopes = scope ? scope.toString().split(",") : [];
|
const scopes = scope ? scope.toString().split(",") : [];
|
||||||
|
|
||||||
const { data: client, isLoading: isLoadingGetClient } = trpc.viewer.oAuth.getClient.useQuery(
|
const { data: client, isPending: isPendingGetClient } = trpc.viewer.oAuth.getClient.useQuery(
|
||||||
{
|
{
|
||||||
clientId: client_id as string,
|
clientId: client_id as string,
|
||||||
},
|
},
|
||||||
@@ -37,7 +37,7 @@ export default function Authorize() {
|
|||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
const { data, isLoading: isLoadingProfiles } = trpc.viewer.teamsAndUserProfilesQuery.useQuery();
|
const { data, isPending: isPendingProfiles } = trpc.viewer.teamsAndUserProfilesQuery.useQuery();
|
||||||
|
|
||||||
const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({
|
const generateAuthCodeMutation = trpc.viewer.oAuth.generateAuthCode.useMutation({
|
||||||
onSuccess: (data) => {
|
onSuccess: (data) => {
|
||||||
@@ -58,7 +58,7 @@ export default function Authorize() {
|
|||||||
if (mappedProfiles.length > 0) {
|
if (mappedProfiles.length > 0) {
|
||||||
setSelectedAccount(mappedProfiles[0]);
|
setSelectedAccount(mappedProfiles[0]);
|
||||||
}
|
}
|
||||||
}, [isLoadingProfiles]);
|
}, [isPendingProfiles]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (status === "unauthenticated") {
|
if (status === "unauthenticated") {
|
||||||
@@ -69,9 +69,9 @@ export default function Authorize() {
|
|||||||
}
|
}
|
||||||
}, [status]);
|
}, [status]);
|
||||||
|
|
||||||
const isLoading = isLoadingGetClient || isLoadingProfiles || status !== "authenticated";
|
const isPending = isPendingGetClient || isPendingProfiles || status !== "authenticated";
|
||||||
|
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,12 @@
|
|||||||
import type { GetServerSidePropsContext } from "next";
|
"use client";
|
||||||
|
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
|
|
||||||
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
import AdminAppsList from "@calcom/features/apps/AdminAppsList";
|
||||||
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
|
||||||
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
|
||||||
import { APP_NAME } from "@calcom/lib/constants";
|
import { APP_NAME } from "@calcom/lib/constants";
|
||||||
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
import { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
|
||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import prisma from "@calcom/prisma";
|
|
||||||
import { UserPermissionRole } from "@calcom/prisma/enums";
|
|
||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { Meta, WizardForm } from "@calcom/ui";
|
import { Meta, WizardForm } from "@calcom/ui";
|
||||||
|
|
||||||
@@ -18,7 +15,7 @@ import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
|
|||||||
import ChooseLicense from "@components/setup/ChooseLicense";
|
import ChooseLicense from "@components/setup/ChooseLicense";
|
||||||
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
|
||||||
|
|
||||||
import { ssrInit } from "@server/lib/ssr";
|
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
|
||||||
|
|
||||||
function useSetStep() {
|
function useSetStep() {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
@@ -44,16 +41,16 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
{
|
{
|
||||||
title: t("administrator_user"),
|
title: t("administrator_user"),
|
||||||
description: t("lets_create_first_administrator_user"),
|
description: t("lets_create_first_administrator_user"),
|
||||||
content: (setIsLoading) => (
|
content: (setIsPending) => (
|
||||||
<AdminUser
|
<AdminUser
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setStep(2);
|
setStep(2);
|
||||||
}}
|
}}
|
||||||
onError={() => {
|
onError={() => {
|
||||||
setIsLoading(false);
|
setIsPending(false);
|
||||||
}}
|
}}
|
||||||
userCount={props.userCount}
|
userCount={props.userCount}
|
||||||
/>
|
/>
|
||||||
@@ -62,7 +59,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
{
|
{
|
||||||
title: t("choose_a_license"),
|
title: t("choose_a_license"),
|
||||||
description: t("choose_license_description"),
|
description: t("choose_license_description"),
|
||||||
content: (setIsLoading) => {
|
content: (setIsPending) => {
|
||||||
return (
|
return (
|
||||||
<ChooseLicense
|
<ChooseLicense
|
||||||
id="wizard-step-2"
|
id="wizard-step-2"
|
||||||
@@ -70,7 +67,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
value={value}
|
value={value}
|
||||||
onChange={setValue}
|
onChange={setValue}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
setStep(3);
|
setStep(3);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -83,14 +80,14 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
steps.push({
|
steps.push({
|
||||||
title: t("step_enterprise_license"),
|
title: t("step_enterprise_license"),
|
||||||
description: t("step_enterprise_license_description"),
|
description: t("step_enterprise_license_description"),
|
||||||
content: (setIsLoading) => {
|
content: (setIsPending) => {
|
||||||
const currentStep = 3;
|
const currentStep = 3;
|
||||||
return (
|
return (
|
||||||
<EnterpriseLicense
|
<EnterpriseLicense
|
||||||
id={`wizard-step-${currentStep}`}
|
id={`wizard-step-${currentStep}`}
|
||||||
name={`wizard-step-${currentStep}`}
|
name={`wizard-step-${currentStep}`}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
}}
|
}}
|
||||||
onSuccess={() => {
|
onSuccess={() => {
|
||||||
setStep(currentStep + 1);
|
setStep(currentStep + 1);
|
||||||
@@ -109,7 +106,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
title: t("enable_apps"),
|
title: t("enable_apps"),
|
||||||
description: t("enable_apps_description", { appName: APP_NAME }),
|
description: t("enable_apps_description", { appName: APP_NAME }),
|
||||||
contentClassname: "!pb-0 mb-[-1px]",
|
contentClassname: "!pb-0 mb-[-1px]",
|
||||||
content: (setIsLoading) => {
|
content: (setIsPending) => {
|
||||||
const currentStep = isFreeLicense ? 3 : 4;
|
const currentStep = isFreeLicense ? 3 : 4;
|
||||||
return (
|
return (
|
||||||
<AdminAppsList
|
<AdminAppsList
|
||||||
@@ -123,7 +120,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
|
|||||||
baseURL={`/auth/setup?step=${currentStep}`}
|
baseURL={`/auth/setup?step=${currentStep}`}
|
||||||
useQueryParam={true}
|
useQueryParam={true}
|
||||||
onSubmit={() => {
|
onSubmit={() => {
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
router.replace("/");
|
router.replace("/");
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
@@ -152,50 +149,4 @@ Setup.isThemeSupported = false;
|
|||||||
Setup.PageWrapper = PageWrapper;
|
Setup.PageWrapper = PageWrapper;
|
||||||
export default Setup;
|
export default Setup;
|
||||||
|
|
||||||
export const getServerSideProps = async (context: GetServerSidePropsContext) => {
|
export { getServerSideProps };
|
||||||
const { req } = context;
|
|
||||||
|
|
||||||
const ssr = await ssrInit(context);
|
|
||||||
const userCount = await prisma.user.count();
|
|
||||||
|
|
||||||
const session = await getServerSession({ req });
|
|
||||||
|
|
||||||
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
|
||||||
return {
|
|
||||||
redirect: {
|
|
||||||
destination: `/404`,
|
|
||||||
permanent: false,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const deploymentKey = await prisma.deployment.findUnique({
|
|
||||||
where: { id: 1 },
|
|
||||||
select: { licenseKey: true },
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
|
||||||
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
|
|
||||||
await prisma.deployment.upsert({
|
|
||||||
where: { id: 1 },
|
|
||||||
update: {
|
|
||||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
|
||||||
agreedLicenseAt: new Date(),
|
|
||||||
},
|
|
||||||
create: {
|
|
||||||
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
|
||||||
agreedLicenseAt: new Date(),
|
|
||||||
},
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
|
|
||||||
|
|
||||||
return {
|
|
||||||
props: {
|
|
||||||
trpcState: ssr.dehydrate(),
|
|
||||||
isFreeLicense,
|
|
||||||
userCount,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@@ -44,7 +44,7 @@ function VerifyEmailPage() {
|
|||||||
<Button
|
<Button
|
||||||
color="minimal"
|
color="minimal"
|
||||||
className="underline"
|
className="underline"
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
showToast("Send email", "success");
|
showToast("Send email", "success");
|
||||||
mutation.mutate();
|
mutation.mutate();
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ export default function Availability() {
|
|||||||
const fromEventType = searchParams?.get("fromEventType");
|
const fromEventType = searchParams?.get("fromEventType");
|
||||||
const { timeFormat } = me.data || { timeFormat: null };
|
const { timeFormat } = me.data || { timeFormat: null };
|
||||||
const [openSidebar, setOpenSidebar] = useState(false);
|
const [openSidebar, setOpenSidebar] = useState(false);
|
||||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
const { data: schedule, isPending } = trpc.viewer.availability.schedule.get.useQuery(
|
||||||
{ scheduleId },
|
{ scheduleId },
|
||||||
{
|
{
|
||||||
enabled: !!scheduleId,
|
enabled: !!scheduleId,
|
||||||
@@ -158,7 +158,7 @@ export default function Availability() {
|
|||||||
control={form.control}
|
control={form.control}
|
||||||
name="name"
|
name="name"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
<EditableHeading isReady={!isLoading} {...field} data-testid="availablity-title" />
|
<EditableHeading isReady={!isPending} {...field} data-testid="availablity-title" />
|
||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
@@ -188,7 +188,7 @@ export default function Availability() {
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
disabled={isLoading || schedule?.isDefault}
|
disabled={isPending || schedule?.isDefault}
|
||||||
checked={form.watch("isDefault")}
|
checked={form.watch("isDefault")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
form.setValue("isDefault", e);
|
form.setValue("isDefault", e);
|
||||||
@@ -210,7 +210,7 @@ export default function Availability() {
|
|||||||
/>
|
/>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isPending={deleteMutation.isPending}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_schedule")}
|
title={t("delete_schedule")}
|
||||||
confirmBtnText={t("delete")}
|
confirmBtnText={t("delete")}
|
||||||
@@ -249,7 +249,7 @@ export default function Availability() {
|
|||||||
/>
|
/>
|
||||||
</DialogTrigger>
|
</DialogTrigger>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isPending={deleteMutation.isPending}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_schedule")}
|
title={t("delete_schedule")}
|
||||||
confirmBtnText={t("delete")}
|
confirmBtnText={t("delete")}
|
||||||
@@ -284,7 +284,7 @@ export default function Availability() {
|
|||||||
</Skeleton>
|
</Skeleton>
|
||||||
<Switch
|
<Switch
|
||||||
id="hiddenSwitch"
|
id="hiddenSwitch"
|
||||||
disabled={isLoading || schedule?.isDefault}
|
disabled={isPending || schedule?.isDefault}
|
||||||
checked={form.watch("isDefault")}
|
checked={form.watch("isDefault")}
|
||||||
onCheckedChange={(e) => {
|
onCheckedChange={(e) => {
|
||||||
form.setValue("isDefault", e);
|
form.setValue("isDefault", e);
|
||||||
@@ -334,7 +334,7 @@ export default function Availability() {
|
|||||||
className="ml-4 lg:ml-0"
|
className="ml-4 lg:ml-0"
|
||||||
type="submit"
|
type="submit"
|
||||||
form="availability-form"
|
form="availability-form"
|
||||||
loading={updateMutation.isLoading}>
|
loading={updateMutation.isPending}>
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import { trpc } from "@calcom/trpc/react";
|
|||||||
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
|
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
|
||||||
import { Clock } from "@calcom/ui/components/icon";
|
import { Clock } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { withQuery } from "@lib/QueryCell";
|
import { QueryCell } from "@lib/QueryCell";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
import SkeletonLoader from "@components/availability/SkeletonLoader";
|
||||||
@@ -136,8 +136,17 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
function AvailabilityListWithQuery() {
|
||||||
const WithQuery = withQuery(trpc.viewer.availability.list as any);
|
const query = trpc.viewer.availability.list.useQuery();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<QueryCell
|
||||||
|
query={query}
|
||||||
|
success={({ data }) => <AvailabilityList {...data} />}
|
||||||
|
customLoader={<SkeletonLoader />}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export default function AvailabilityPage() {
|
export default function AvailabilityPage() {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -179,14 +188,7 @@ export default function AvailabilityPage() {
|
|||||||
<NewScheduleButton />
|
<NewScheduleButton />
|
||||||
</div>
|
</div>
|
||||||
}>
|
}>
|
||||||
{searchParams?.get("type") === "team" ? (
|
{searchParams?.get("type") === "team" ? <AvailabilitySliderTable /> : <AvailabilityListWithQuery />}
|
||||||
<AvailabilitySliderTable />
|
|
||||||
) : (
|
|
||||||
<WithQuery
|
|
||||||
success={({ data }) => <AvailabilityList {...data} />}
|
|
||||||
customLoader={<SkeletonLoader />}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
</ShellMain>
|
</ShellMain>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -164,7 +164,7 @@ export default function Bookings() {
|
|||||||
{query.status === "error" && (
|
{query.status === "error" && (
|
||||||
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
<Alert severity="error" title={t("something_went_wrong")} message={query.error.message} />
|
||||||
)}
|
)}
|
||||||
{(query.status === "loading" || query.isPaused) && <SkeletonLoader />}
|
{(query.status === "pending" || query.isPaused) && <SkeletonLoader />}
|
||||||
{query.status === "success" && !isEmpty && (
|
{query.status === "success" && !isEmpty && (
|
||||||
<>
|
<>
|
||||||
{!!bookingsToday.length && status === "upcoming" && (
|
{!!bookingsToday.length && status === "upcoming" && (
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ function ConnectAndJoin() {
|
|||||||
</div>
|
</div>
|
||||||
) : (
|
) : (
|
||||||
<Button
|
<Button
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isPending}
|
||||||
tooltip={isUserPartOfOrg ? t("join_meeting") : t("not_part_of_org")}
|
tooltip={isUserPartOfOrg ? t("join_meeting") : t("not_part_of_org")}
|
||||||
disabled={!isUserPartOfOrg}
|
disabled={!isUserPartOfOrg}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
|||||||
@@ -80,6 +80,7 @@ const EventWebhooksTab = dynamic(() =>
|
|||||||
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
|
||||||
|
|
||||||
export type FormValues = {
|
export type FormValues = {
|
||||||
|
id: number;
|
||||||
title: string;
|
title: string;
|
||||||
eventTitle: string;
|
eventTitle: string;
|
||||||
eventName: string;
|
eventName: string;
|
||||||
@@ -119,6 +120,7 @@ export type FormValues = {
|
|||||||
seatsShowAttendees: boolean | null;
|
seatsShowAttendees: boolean | null;
|
||||||
seatsShowAvailabilityCount: boolean | null;
|
seatsShowAvailabilityCount: boolean | null;
|
||||||
seatsPerTimeSlotEnabled: boolean;
|
seatsPerTimeSlotEnabled: boolean;
|
||||||
|
scheduleName: string;
|
||||||
minimumBookingNotice: number;
|
minimumBookingNotice: number;
|
||||||
minimumBookingNoticeInDurationType: number;
|
minimumBookingNoticeInDurationType: number;
|
||||||
beforeBufferTime: number;
|
beforeBufferTime: number;
|
||||||
@@ -139,6 +141,7 @@ export type FormValues = {
|
|||||||
availability?: AvailabilityOption;
|
availability?: AvailabilityOption;
|
||||||
bookerLayouts: BookerLayoutSettings;
|
bookerLayouts: BookerLayoutSettings;
|
||||||
multipleDurationEnabled: boolean;
|
multipleDurationEnabled: boolean;
|
||||||
|
users: EventTypeSetup["users"];
|
||||||
};
|
};
|
||||||
|
|
||||||
export type CustomInputParsed = typeof customInputSchema._output;
|
export type CustomInputParsed = typeof customInputSchema._output;
|
||||||
@@ -180,7 +183,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
|
|
||||||
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
|
||||||
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
|
||||||
|
|
||||||
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
|
||||||
onSuccess: async () => {
|
onSuccess: async () => {
|
||||||
formMethods.setValue(
|
formMethods.setValue(
|
||||||
@@ -248,6 +250,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
const defaultValues: any = useMemo(() => {
|
const defaultValues: any = useMemo(() => {
|
||||||
return {
|
return {
|
||||||
title: eventType.title,
|
title: eventType.title,
|
||||||
|
id: eventType.id,
|
||||||
|
slug: eventType.slug,
|
||||||
|
afterEventBuffer: eventType.afterEventBuffer,
|
||||||
|
beforeEventBuffer: eventType.beforeEventBuffer,
|
||||||
|
eventName: eventType.eventName || "",
|
||||||
|
scheduleName: eventType.scheduleName,
|
||||||
|
periodDays: eventType.periodDays || 30,
|
||||||
|
requiresBookerEmailVerification: eventType.requiresBookerEmailVerification,
|
||||||
|
seatsPerTimeSlot: eventType.seatsPerTimeSlot,
|
||||||
|
seatsShowAttendees: eventType.seatsShowAttendees,
|
||||||
|
seatsShowAvailabilityCount: eventType.seatsShowAvailabilityCount,
|
||||||
|
lockTimeZoneToggleOnBookingPage: eventType.lockTimeZoneToggleOnBookingPage,
|
||||||
locations: eventType.locations || [],
|
locations: eventType.locations || [],
|
||||||
recurringEvent: eventType.recurringEvent || null,
|
recurringEvent: eventType.recurringEvent || null,
|
||||||
isInstantEvent: eventType.isInstantEvent,
|
isInstantEvent: eventType.isInstantEvent,
|
||||||
@@ -268,9 +282,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
periodType: eventType.periodType,
|
periodType: eventType.periodType,
|
||||||
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
|
||||||
schedulingType: eventType.schedulingType,
|
schedulingType: eventType.schedulingType,
|
||||||
|
requiresConfirmation: eventType.requiresConfirmation,
|
||||||
|
slotInterval: eventType.slotInterval,
|
||||||
minimumBookingNotice: eventType.minimumBookingNotice,
|
minimumBookingNotice: eventType.minimumBookingNotice,
|
||||||
metadata,
|
metadata,
|
||||||
hosts: eventType.hosts,
|
hosts: eventType.hosts,
|
||||||
|
successRedirectUrl: eventType.successRedirectUrl || "",
|
||||||
|
users: eventType.users,
|
||||||
children: eventType.children.map((ch) => ({
|
children: eventType.children.map((ch) => ({
|
||||||
...ch,
|
...ch,
|
||||||
created: true,
|
created: true,
|
||||||
@@ -411,7 +429,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
),
|
),
|
||||||
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
|
||||||
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
|
||||||
limits: <EventLimitsTab eventType={eventType} />,
|
limits: <EventLimitsTab />,
|
||||||
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
|
||||||
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
|
||||||
recurring: <EventRecurringTab eventType={eventType} />,
|
recurring: <EventRecurringTab eventType={eventType} />,
|
||||||
@@ -490,7 +508,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { availability, ...rest } = input;
|
const { availability, users, scheduleName, ...rest } = input;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...rest,
|
...rest,
|
||||||
length,
|
length,
|
||||||
@@ -550,9 +568,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
installedAppsNumber={eventTypeApps?.items.length || 0}
|
installedAppsNumber={eventTypeApps?.items.length || 0}
|
||||||
enabledWorkflowsNumber={eventType.workflows.length}
|
enabledWorkflowsNumber={eventType.workflows.length}
|
||||||
eventType={eventType}
|
eventType={eventType}
|
||||||
|
activeWebhooksNumber={eventType.webhooks.filter((webhook) => webhook.active).length}
|
||||||
team={team}
|
team={team}
|
||||||
availability={availability}
|
availability={availability}
|
||||||
isUpdateMutationLoading={updateMutation.isLoading}
|
isUpdateMutationLoading={updateMutation.isPending}
|
||||||
formMethods={formMethods}
|
formMethods={formMethods}
|
||||||
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
|
||||||
disableBorder={true}
|
disableBorder={true}
|
||||||
@@ -618,7 +637,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
throw new Error(t("event_setup_multiple_payment_apps_error"));
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
||||||
const { availability, ...rest } = input;
|
const { availability, users, scheduleName, ...rest } = input;
|
||||||
updateMutation.mutate({
|
updateMutation.mutate({
|
||||||
...rest,
|
...rest,
|
||||||
length,
|
length,
|
||||||
@@ -647,7 +666,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
|
|||||||
{slugExistsChildrenDialogOpen.length ? (
|
{slugExistsChildrenDialogOpen.length ? (
|
||||||
<ManagedEventTypeDialog
|
<ManagedEventTypeDialog
|
||||||
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
|
||||||
isLoading={formMethods.formState.isSubmitting}
|
isPending={formMethods.formState.isSubmitting}
|
||||||
onOpenChange={() => {
|
onOpenChange={() => {
|
||||||
setSlugExistsChildrenDialogOpen([]);
|
setSlugExistsChildrenDialogOpen([]);
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -54,6 +54,7 @@ import {
|
|||||||
Switch,
|
Switch,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
ArrowButton,
|
ArrowButton,
|
||||||
|
UserAvatarGroup,
|
||||||
} from "@calcom/ui";
|
} from "@calcom/ui";
|
||||||
import {
|
import {
|
||||||
Clipboard,
|
Clipboard,
|
||||||
@@ -74,7 +75,6 @@ import useMeQuery from "@lib/hooks/useMeQuery";
|
|||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
|
||||||
import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup";
|
|
||||||
|
|
||||||
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
|
||||||
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
|
||||||
@@ -686,7 +686,7 @@ export const EventTypeList = ({
|
|||||||
title={t(`delete${isManagedEventPrefix()}_event_type`)}
|
title={t(`delete${isManagedEventPrefix()}_event_type`)}
|
||||||
confirmBtnText={t(`confirm_delete_event_type`)}
|
confirmBtnText={t(`confirm_delete_event_type`)}
|
||||||
loadingText={t(`confirm_delete_event_type`)}
|
loadingText={t(`confirm_delete_event_type`)}
|
||||||
isLoading={deleteMutation.isLoading}
|
isPending={deleteMutation.isPending}
|
||||||
onConfirm={(e) => {
|
onConfirm={(e) => {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
deleteEventTypeHandler(deleteDialogTypeId);
|
deleteEventTypeHandler(deleteDialogTypeId);
|
||||||
@@ -856,7 +856,7 @@ const Main = ({
|
|||||||
const searchParams = useCompatSearchParams();
|
const searchParams = useCompatSearchParams();
|
||||||
const orgBranding = useOrgBranding();
|
const orgBranding = useOrgBranding();
|
||||||
|
|
||||||
if (!data || status === "loading") {
|
if (!data || status === "pending") {
|
||||||
return <SkeletonLoader />;
|
return <SkeletonLoader />;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -934,7 +934,7 @@ const EventTypesPage = () => {
|
|||||||
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
// TODO: Maybe useSuspenseQuery to focus on success case only? Remember that it would crash the page when there is an error in query. Also, it won't support skeleton
|
||||||
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
|
||||||
refetchOnWindowFocus: false,
|
refetchOnWindowFocus: false,
|
||||||
cacheTime: 1 * 60 * 60 * 1000,
|
gcTime: 1 * 60 * 60 * 1000,
|
||||||
staleTime: 1 * 60 * 60 * 1000,
|
staleTime: 1 * 60 * 60 * 1000,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
|
|||||||
const ApiKeysView = () => {
|
const ApiKeysView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const { data, isLoading } = trpc.viewer.apiKeys.list.useQuery();
|
const { data, isPending } = trpc.viewer.apiKeys.list.useQuery();
|
||||||
|
|
||||||
const [apiKeyModal, setApiKeyModal] = useState(false);
|
const [apiKeyModal, setApiKeyModal] = useState(false);
|
||||||
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>(
|
const [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>(
|
||||||
@@ -57,7 +57,7 @@ const ApiKeysView = () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading || !data) {
|
if (isPending || !data) {
|
||||||
return (
|
return (
|
||||||
<SkeletonLoader
|
<SkeletonLoader
|
||||||
title={t("api_keys")}
|
title={t("api_keys")}
|
||||||
|
|||||||
@@ -317,7 +317,7 @@ const AppearanceView = ({
|
|||||||
<SettingsToggle
|
<SettingsToggle
|
||||||
toggleSwitchAtTheEnd={true}
|
toggleSwitchAtTheEnd={true}
|
||||||
title={t("disable_cal_branding", { appName: APP_NAME })}
|
title={t("disable_cal_branding", { appName: APP_NAME })}
|
||||||
disabled={!hasPaidPlan || mutation?.isLoading}
|
disabled={!hasPaidPlan || mutation?.isPending}
|
||||||
description={t("removes_cal_branding", { appName: APP_NAME })}
|
description={t("removes_cal_branding", { appName: APP_NAME })}
|
||||||
checked={hasPaidPlan ? hideBrandingValue : false}
|
checked={hasPaidPlan ? hideBrandingValue : false}
|
||||||
Badge={<UpgradeTeamsBadge />}
|
Badge={<UpgradeTeamsBadge />}
|
||||||
@@ -332,12 +332,12 @@ const AppearanceView = ({
|
|||||||
};
|
};
|
||||||
|
|
||||||
const AppearanceViewWrapper = () => {
|
const AppearanceViewWrapper = () => {
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading || isTeamPlanStatusLoading || !user)
|
if (isPending || isTeamPlanStatusLoading || !user)
|
||||||
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
|
||||||
|
|
||||||
return <AppearanceView user={user} hasPaidPlan={hasPaidPlan} />;
|
return <AppearanceView user={user} hasPaidPlan={hasPaidPlan} />;
|
||||||
|
|||||||
@@ -51,8 +51,8 @@ interface GeneralViewProps {
|
|||||||
const GeneralQueryView = () => {
|
const GeneralQueryView = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
|
if (isPending) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
throw new Error(t("something_went_wrong"));
|
throw new Error(t("something_went_wrong"));
|
||||||
}
|
}
|
||||||
@@ -233,7 +233,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
toggleSwitchAtTheEnd={true}
|
toggleSwitchAtTheEnd={true}
|
||||||
title={t("dynamic_booking")}
|
title={t("dynamic_booking")}
|
||||||
description={t("allow_dynamic_booking")}
|
description={t("allow_dynamic_booking")}
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isPending}
|
||||||
checked={isAllowDynamicBookingChecked}
|
checked={isAllowDynamicBookingChecked}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setIsAllowDynamicBookingChecked(checked);
|
setIsAllowDynamicBookingChecked(checked);
|
||||||
@@ -246,7 +246,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
toggleSwitchAtTheEnd={true}
|
toggleSwitchAtTheEnd={true}
|
||||||
title={t("seo_indexing")}
|
title={t("seo_indexing")}
|
||||||
description={t("allow_seo_indexing")}
|
description={t("allow_seo_indexing")}
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isPending}
|
||||||
checked={isAllowSEOIndexingChecked}
|
checked={isAllowSEOIndexingChecked}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setIsAllowSEOIndexingChecked(checked);
|
setIsAllowSEOIndexingChecked(checked);
|
||||||
@@ -259,7 +259,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
|
|||||||
toggleSwitchAtTheEnd={true}
|
toggleSwitchAtTheEnd={true}
|
||||||
title={t("monthly_digest_email")}
|
title={t("monthly_digest_email")}
|
||||||
description={t("monthly_digest_email_for_teams")}
|
description={t("monthly_digest_email_for_teams")}
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isPending}
|
||||||
checked={isReceiveMonthlyDigestEmailChecked}
|
checked={isReceiveMonthlyDigestEmailChecked}
|
||||||
onCheckedChange={(checked) => {
|
onCheckedChange={(checked) => {
|
||||||
setIsReceiveMonthlyDigestEmailChecked(checked);
|
setIsReceiveMonthlyDigestEmailChecked(checked);
|
||||||
|
|||||||
@@ -155,7 +155,7 @@ const OutOfOfficeSection = () => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
disabled={createOutOfOfficeEntry.isLoading}
|
disabled={createOutOfOfficeEntry.isPending}
|
||||||
data-testid="create-entry-ooo-redirect">
|
data-testid="create-entry-ooo-redirect">
|
||||||
{t("create_entry")}
|
{t("create_entry")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -168,7 +168,7 @@ const OutOfOfficeSection = () => {
|
|||||||
const OutOfOfficeEntriesList = () => {
|
const OutOfOfficeEntriesList = () => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { data, isLoading } = trpc.viewer.outOfOfficeEntriesList.useQuery();
|
const { data, isPending } = trpc.viewer.outOfOfficeEntriesList.useQuery();
|
||||||
const deleteOutOfOfficeEntryMutation = trpc.viewer.outOfOfficeEntryDelete.useMutation({
|
const deleteOutOfOfficeEntryMutation = trpc.viewer.outOfOfficeEntryDelete.useMutation({
|
||||||
onSuccess: () => {
|
onSuccess: () => {
|
||||||
showToast(t("success_deleted_entry_out_of_office"), "success");
|
showToast(t("success_deleted_entry_out_of_office"), "success");
|
||||||
@@ -207,7 +207,7 @@ const OutOfOfficeEntriesList = () => {
|
|||||||
type="button"
|
type="button"
|
||||||
color="minimal"
|
color="minimal"
|
||||||
variant="icon"
|
variant="icon"
|
||||||
disabled={deleteOutOfOfficeEntryMutation.isLoading}
|
disabled={deleteOutOfOfficeEntryMutation.isPending}
|
||||||
StartIcon={Trash2}
|
StartIcon={Trash2}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
|
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
|
||||||
@@ -216,7 +216,7 @@ const OutOfOfficeEntriesList = () => {
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
{isLoading && (
|
{isPending && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{new Array(6).fill(0).map((_, index) => (
|
{new Array(6).fill(0).map((_, index) => (
|
||||||
<TableCell key={index}>
|
<TableCell key={index}>
|
||||||
@@ -226,7 +226,7 @@ const OutOfOfficeEntriesList = () => {
|
|||||||
</TableRow>
|
</TableRow>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{!isLoading && (data === undefined || data.length === 0) && (
|
{!isPending && (data === undefined || data.length === 0) && (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6} className="text-center">
|
<TableCell colSpan={6} className="text-center">
|
||||||
<p className="text-subtle text-sm">{t("no_redirects_found")}</p>
|
<p className="text-subtle text-sm">{t("no_redirects_found")}</p>
|
||||||
|
|||||||
@@ -84,10 +84,10 @@ const ProfileView = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
const { update } = useSession();
|
const { update } = useSession();
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
|
|
||||||
const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
|
const { data: avatarData } = trpc.viewer.avatar.useQuery(undefined, {
|
||||||
enabled: !isLoading && !user?.avatarUrl,
|
enabled: !isPending && !user?.avatarUrl,
|
||||||
});
|
});
|
||||||
|
|
||||||
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
|
||||||
@@ -222,7 +222,7 @@ const ProfileView = () => {
|
|||||||
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if (isLoading || !user) {
|
if (isPending || !user) {
|
||||||
return (
|
return (
|
||||||
<SkeletonLoader title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
<SkeletonLoader title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
|
||||||
);
|
);
|
||||||
@@ -246,7 +246,7 @@ const ProfileView = () => {
|
|||||||
<ProfileForm
|
<ProfileForm
|
||||||
key={JSON.stringify(defaultValues)}
|
key={JSON.stringify(defaultValues)}
|
||||||
defaultValues={defaultValues}
|
defaultValues={defaultValues}
|
||||||
isLoading={updateProfileMutation.isLoading}
|
isPending={updateProfileMutation.isPending}
|
||||||
isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
|
isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
|
||||||
user={user}
|
user={user}
|
||||||
userOrganization={user.organization}
|
userOrganization={user.organization}
|
||||||
@@ -354,7 +354,7 @@ const ProfileView = () => {
|
|||||||
<DialogFooter showDivider>
|
<DialogFooter showDivider>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
loading={confirmPasswordMutation.isLoading}
|
loading={confirmPasswordMutation.isPending}
|
||||||
onClick={(e) => onConfirmPassword(e)}>
|
onClick={(e) => onConfirmPassword(e)}>
|
||||||
{t("confirm")}
|
{t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -375,7 +375,7 @@ const ProfileView = () => {
|
|||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
loading={updateProfileMutation.isLoading}
|
loading={updateProfileMutation.isPending}
|
||||||
onClick={(e) => onConfirmAuthEmailChange(e)}>
|
onClick={(e) => onConfirmAuthEmailChange(e)}>
|
||||||
{t("confirm")}
|
{t("confirm")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -391,7 +391,7 @@ const ProfileForm = ({
|
|||||||
defaultValues,
|
defaultValues,
|
||||||
onSubmit,
|
onSubmit,
|
||||||
extraField,
|
extraField,
|
||||||
isLoading = false,
|
isPending = false,
|
||||||
isFallbackImg,
|
isFallbackImg,
|
||||||
user,
|
user,
|
||||||
userOrganization,
|
userOrganization,
|
||||||
@@ -399,7 +399,7 @@ const ProfileForm = ({
|
|||||||
defaultValues: FormValues;
|
defaultValues: FormValues;
|
||||||
onSubmit: (values: FormValues) => void;
|
onSubmit: (values: FormValues) => void;
|
||||||
extraField?: React.ReactNode;
|
extraField?: React.ReactNode;
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
isFallbackImg: boolean;
|
isFallbackImg: boolean;
|
||||||
user: RouterOutputs["viewer"]["me"];
|
user: RouterOutputs["viewer"]["me"];
|
||||||
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
|
||||||
@@ -509,7 +509,7 @@ const ProfileForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<SectionBottomActions align="end">
|
<SectionBottomActions align="end">
|
||||||
<Button loading={isLoading} disabled={isDisabled} color="primary" type="submit">
|
<Button loading={isPending} disabled={isDisabled} color="primary" type="submit">
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
</SectionBottomActions>
|
</SectionBottomActions>
|
||||||
|
|||||||
@@ -69,7 +69,7 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
|||||||
mutation.mutate({ disableImpersonation: !checked });
|
mutation.mutate({ disableImpersonation: !checked });
|
||||||
}}
|
}}
|
||||||
switchContainerClassName="rounded-t-none border-t-0"
|
switchContainerClassName="rounded-t-none border-t-0"
|
||||||
disabled={mutation.isLoading}
|
disabled={mutation.isPending}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</>
|
</>
|
||||||
@@ -77,10 +77,10 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileImpersonationViewWrapper = () => {
|
const ProfileImpersonationViewWrapper = () => {
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading || !user)
|
if (isPending || !user)
|
||||||
return <SkeletonLoader title={t("impersonation")} description={t("impersonation_description")} />;
|
return <SkeletonLoader title={t("impersonation")} description={t("impersonation_description")} />;
|
||||||
|
|
||||||
return <ProfileImpersonationView user={user} />;
|
return <ProfileImpersonationView user={user} />;
|
||||||
|
|||||||
@@ -215,9 +215,9 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
type="submit"
|
type="submit"
|
||||||
loading={passwordMutation.isLoading}
|
loading={passwordMutation.isPending}
|
||||||
onClick={() => formMethods.clearErrors("apiError")}
|
onClick={() => formMethods.clearErrors("apiError")}
|
||||||
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
|
disabled={isDisabled || passwordMutation.isPending || sessionMutation.isPending}>
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
</SectionBottomActions>
|
</SectionBottomActions>
|
||||||
@@ -268,7 +268,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||||||
<SectionBottomActions align="end">
|
<SectionBottomActions align="end">
|
||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
loading={sessionMutation.isLoading}
|
loading={sessionMutation.isPending}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
sessionMutation.mutate({
|
sessionMutation.mutate({
|
||||||
metadata: { ...metadata, sessionTimeout },
|
metadata: { ...metadata, sessionTimeout },
|
||||||
@@ -277,8 +277,8 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||||||
}}
|
}}
|
||||||
disabled={
|
disabled={
|
||||||
initialSessionTimeout === sessionTimeout ||
|
initialSessionTimeout === sessionTimeout ||
|
||||||
passwordMutation.isLoading ||
|
passwordMutation.isPending ||
|
||||||
sessionMutation.isLoading
|
sessionMutation.isPending
|
||||||
}>
|
}>
|
||||||
{t("update")}
|
{t("update")}
|
||||||
</Button>
|
</Button>
|
||||||
@@ -293,9 +293,9 @@ const PasswordView = ({ user }: PasswordViewProps) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const PasswordViewWrapper = () => {
|
const PasswordViewWrapper = () => {
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
if (isLoading || !user)
|
if (isPending || !user)
|
||||||
return <SkeletonLoader title={t("password")} description={t("password_description")} />;
|
return <SkeletonLoader title={t("password")} description={t("password_description")} />;
|
||||||
|
|
||||||
return <PasswordView user={user} />;
|
return <PasswordView user={user} />;
|
||||||
|
|||||||
@@ -37,12 +37,12 @@ const TwoFactorAuthView = () => {
|
|||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { data: user, isLoading } = trpc.viewer.me.useQuery();
|
const { data: user, isPending } = trpc.viewer.me.useQuery();
|
||||||
|
|
||||||
const [enableModalOpen, setEnableModalOpen] = useState<boolean>(false);
|
const [enableModalOpen, setEnableModalOpen] = useState<boolean>(false);
|
||||||
const [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
|
const [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
|
||||||
|
|
||||||
if (isLoading)
|
if (isPending)
|
||||||
return <SkeletonLoader title={t("2fa")} description={t("set_up_two_factor_authentication")} />;
|
return <SkeletonLoader title={t("2fa")} description={t("set_up_two_factor_authentication")} />;
|
||||||
|
|
||||||
const isCalProvider = user?.identityProvider === "CAL";
|
const isCalProvider = user?.identityProvider === "CAL";
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
|
|||||||
import useTheme from "@calcom/lib/hooks/useTheme";
|
import useTheme from "@calcom/lib/hooks/useTheme";
|
||||||
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
|
||||||
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
import { teamMetadataSchema } from "@calcom/prisma/zod-utils";
|
||||||
import { Avatar, Button, HeadSeo, UnpublishedEntity } from "@calcom/ui";
|
import { Avatar, Button, HeadSeo, UnpublishedEntity, UserAvatarGroup } from "@calcom/ui";
|
||||||
import { ArrowRight } from "@calcom/ui/components/icon";
|
import { ArrowRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
import { useToggleQuery } from "@lib/hooks/useToggleQuery";
|
||||||
@@ -28,7 +28,6 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
|
|||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
import Team from "@components/team/screens/Team";
|
import Team from "@components/team/screens/Team";
|
||||||
import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup";
|
|
||||||
|
|
||||||
export { getServerSideProps };
|
export { getServerSideProps };
|
||||||
|
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
|
|||||||
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
|
||||||
import { ChevronRight } from "@calcom/ui/components/icon";
|
import { ChevronRight } from "@calcom/ui/components/icon";
|
||||||
|
|
||||||
import type { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
|
import { getServerSideProps } from "@lib/video/[uid]/getServerSideProps";
|
||||||
|
|
||||||
import PageWrapper from "@components/PageWrapper";
|
import PageWrapper from "@components/PageWrapper";
|
||||||
|
|
||||||
@@ -247,4 +247,5 @@ export function VideoMeetingInfo(props: VideoMeetingInfo) {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export { getServerSideProps };
|
||||||
JoinCall.PageWrapper = PageWrapper;
|
JoinCall.PageWrapper = PageWrapper;
|
||||||
@@ -0,0 +1,107 @@
|
|||||||
|
/* eslint-disable playwright/no-conditional-in-test */
|
||||||
|
import { loginUser } from "../fixtures/regularBookings";
|
||||||
|
import { test } from "../lib/fixtures";
|
||||||
|
|
||||||
|
const resolutions = [
|
||||||
|
{
|
||||||
|
width: 1920,
|
||||||
|
height: 1080,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 1280,
|
||||||
|
height: 720,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
width: 640,
|
||||||
|
height: 480,
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
test.describe("Booking With All Questions", () => {
|
||||||
|
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||||
|
await loginUser(users);
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await bookingPage.goToEventType("30 min");
|
||||||
|
await bookingPage.goToTab("event_advanced_tab_title");
|
||||||
|
|
||||||
|
const allQuestions = [
|
||||||
|
"phone",
|
||||||
|
"address",
|
||||||
|
"checkbox",
|
||||||
|
"boolean",
|
||||||
|
"textarea",
|
||||||
|
"multiemail",
|
||||||
|
"multiselect",
|
||||||
|
"number",
|
||||||
|
"radio",
|
||||||
|
"select",
|
||||||
|
"text",
|
||||||
|
];
|
||||||
|
for (const question of allQuestions) {
|
||||||
|
if (
|
||||||
|
question !== "number" &&
|
||||||
|
question !== "select" &&
|
||||||
|
question !== "checkbox" &&
|
||||||
|
question !== "boolean" &&
|
||||||
|
question !== "multiselect" &&
|
||||||
|
question !== "radio"
|
||||||
|
) {
|
||||||
|
await bookingPage.addQuestion(
|
||||||
|
question,
|
||||||
|
`${question}-test`,
|
||||||
|
`${question} test`,
|
||||||
|
true,
|
||||||
|
`${question} test`
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
await bookingPage.addQuestion(question, `${question}-test`, `${question} test`, true);
|
||||||
|
}
|
||||||
|
await bookingPage.checkField(question);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const resolution of resolutions) {
|
||||||
|
test(`Booking with ${resolution.width}x${resolution.height} resolution and all questions`, async ({
|
||||||
|
bookingPage,
|
||||||
|
}) => {
|
||||||
|
await bookingPage.setResolution(resolution.width, resolution.height);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking With no questions", () => {
|
||||||
|
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||||
|
await loginUser(users);
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await bookingPage.goToEventType("30 min");
|
||||||
|
await bookingPage.goToTab("event_advanced_tab_title");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const resolution of resolutions) {
|
||||||
|
test(`Booking with ${resolution.width}x${resolution.height} resolution and no questions`, async ({
|
||||||
|
bookingPage,
|
||||||
|
}) => {
|
||||||
|
await bookingPage.setResolution(resolution.width, resolution.height);
|
||||||
|
await bookingPage.updateEventType();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test.describe("Booking page with no questions", () => {
|
||||||
|
test.beforeEach(async ({ page, users, bookingPage }) => {
|
||||||
|
await loginUser(users);
|
||||||
|
await page.goto("/event-types");
|
||||||
|
await bookingPage.goToEventType("30 min");
|
||||||
|
await bookingPage.goToTab("event_advanced_tab_title");
|
||||||
|
});
|
||||||
|
|
||||||
|
for (const resolution of resolutions) {
|
||||||
|
test(`Booking page with ${resolution.width}x${resolution.height} resolution`, async ({ bookingPage }) => {
|
||||||
|
const eventTypePage = await bookingPage.previewEventType();
|
||||||
|
await eventTypePage.setViewportSize({ width: resolution.width, height: resolution.height });
|
||||||
|
await bookingPage.selectTimeSlot(eventTypePage);
|
||||||
|
await bookingPage.confirmBooking(eventTypePage);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
@@ -207,6 +207,9 @@ export function createBookingPageFixture(page: Page) {
|
|||||||
goToTab: async (tabName: string) => {
|
goToTab: async (tabName: string) => {
|
||||||
await page.getByTestId(`vertical-tab-${tabName}`).click();
|
await page.getByTestId(`vertical-tab-${tabName}`).click();
|
||||||
},
|
},
|
||||||
|
setResolution: async (width: number, height: number) => {
|
||||||
|
await page.setViewportSize({ width: width, height: height });
|
||||||
|
},
|
||||||
addQuestion: async (
|
addQuestion: async (
|
||||||
questionType: string,
|
questionType: string,
|
||||||
identifier: string,
|
identifier: string,
|
||||||
@@ -325,6 +328,14 @@ export function createBookingPageFixture(page: Page) {
|
|||||||
await expect(eventTypePage.getByTestId("cancelled-headline")).toBeVisible();
|
await expect(eventTypePage.getByTestId("cancelled-headline")).toBeVisible();
|
||||||
},
|
},
|
||||||
|
|
||||||
|
confirmBooking: async (eventTypePage: Page) => {
|
||||||
|
const confirmButton = "confirm-book-button";
|
||||||
|
await eventTypePage.getByTestId(confirmButton).click();
|
||||||
|
const scheduleSuccessfullyPage = eventTypePage.getByText(scheduleSuccessfullyText);
|
||||||
|
await scheduleSuccessfullyPage.waitFor({ state: "visible" });
|
||||||
|
await expect(scheduleSuccessfullyPage).toBeVisible();
|
||||||
|
},
|
||||||
|
|
||||||
fillAndConfirmBooking: async ({
|
fillAndConfirmBooking: async ({
|
||||||
eventTypePage,
|
eventTypePage,
|
||||||
placeholderText,
|
placeholderText,
|
||||||
|
|||||||
@@ -1066,6 +1066,8 @@
|
|||||||
"user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.",
|
"user_impersonation_description": "Allows our support team to temporarily sign in as you to help us quickly resolve any issues you report to us.",
|
||||||
"team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.",
|
"team_impersonation_description": "Allows your team Owners/Admins to temporarily sign in as you.",
|
||||||
"cal_signup_description":"Free for individuals. Team plans for collaborative features.",
|
"cal_signup_description":"Free for individuals. Team plans for collaborative features.",
|
||||||
|
"make_org_private": "Make organization private",
|
||||||
|
"make_org_private_description": "Your organization members won't be able to see other organization members when this is turned on.",
|
||||||
"make_team_private": "Make team private",
|
"make_team_private": "Make team private",
|
||||||
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
"make_team_private_description": "Your team members won't be able to see other team members when this is turned on.",
|
||||||
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
"you_cannot_see_team_members": "You cannot see all the team members of a private team.",
|
||||||
|
|||||||
@@ -302,6 +302,7 @@
|
|||||||
"welcome_to_calcom": "ברוך הבא אל {{appName}}",
|
"welcome_to_calcom": "ברוך הבא אל {{appName}}",
|
||||||
"welcome_instructions": "אמור לנו באיזה שם ברצונך שנקרא לך ובאיזה אזור זמן אתה נמצא. ניתן יהיה לערוך את הפרטים האלה מאוחר יותר.",
|
"welcome_instructions": "אמור לנו באיזה שם ברצונך שנקרא לך ובאיזה אזור זמן אתה נמצא. ניתן יהיה לערוך את הפרטים האלה מאוחר יותר.",
|
||||||
"connect_caldav": "חיבור לשרת CalDav (בטא)",
|
"connect_caldav": "חיבור לשרת CalDav (בטא)",
|
||||||
|
"connect_ics_feed": "חיבור לערוץ ICS",
|
||||||
"connect": "להתחבר",
|
"connect": "להתחבר",
|
||||||
"try_for_free": "לנסות בחינם",
|
"try_for_free": "לנסות בחינם",
|
||||||
"create_booking_link_with_calcom": "צור קישור תזמון משלך עם {{appName}}",
|
"create_booking_link_with_calcom": "צור קישור תזמון משלך עם {{appName}}",
|
||||||
@@ -555,6 +556,7 @@
|
|||||||
"enter_number_between_range": "נא להזין מספר בין 1 ל-{{maxOccurences}}",
|
"enter_number_between_range": "נא להזין מספר בין 1 ל-{{maxOccurences}}",
|
||||||
"email_address": "כתובת דוא\"ל",
|
"email_address": "כתובת דוא\"ל",
|
||||||
"enter_valid_email": "יש להזין כתובת דוא״ל תקינה",
|
"enter_valid_email": "יש להזין כתובת דוא״ל תקינה",
|
||||||
|
"please_schedule_future_call": " נא לתזמן שיחה עתידית אם איננו זמינים תוך {{seconds}} שניות",
|
||||||
"location": "מיקום",
|
"location": "מיקום",
|
||||||
"address": "כתובת",
|
"address": "כתובת",
|
||||||
"enter_address": "יש להזין כתובת",
|
"enter_address": "יש להזין כתובת",
|
||||||
@@ -614,6 +616,7 @@
|
|||||||
"new_member": "חבר/ה חדש/ה",
|
"new_member": "חבר/ה חדש/ה",
|
||||||
"invite": "להזמין",
|
"invite": "להזמין",
|
||||||
"add_team_members": "הוספת חברי צוות",
|
"add_team_members": "הוספת חברי צוות",
|
||||||
|
"add_org_members": "הוספת חברים",
|
||||||
"add_team_members_description": "להזמין עוד אנשים להצטרף לצוות",
|
"add_team_members_description": "להזמין עוד אנשים להצטרף לצוות",
|
||||||
"add_team_member": "הוספת חבר צוות",
|
"add_team_member": "הוספת חבר צוות",
|
||||||
"invite_new_member": "להזמין חבר/ת צוות חדש/ה",
|
"invite_new_member": "להזמין חבר/ת צוות חדש/ה",
|
||||||
@@ -1063,6 +1066,8 @@
|
|||||||
"user_impersonation_description": "מצב זה מאפשר לצוות התמיכה שלנו להתחבר באופן זמני לחשבונך כדי שנוכל לפתור במהירות את הבעיות שתדווח/י לנו עליהם.",
|
"user_impersonation_description": "מצב זה מאפשר לצוות התמיכה שלנו להתחבר באופן זמני לחשבונך כדי שנוכל לפתור במהירות את הבעיות שתדווח/י לנו עליהם.",
|
||||||
"team_impersonation_description": "מאפשר לבעלים של הצוות/מנהלים להיכנס זמנית בשמך.",
|
"team_impersonation_description": "מאפשר לבעלים של הצוות/מנהלים להיכנס זמנית בשמך.",
|
||||||
"cal_signup_description": "בחינם למשתמשים פרטיים. התוכנית לצוותים מאפשרת יכולות לשיתופי פעולה.",
|
"cal_signup_description": "בחינם למשתמשים פרטיים. התוכנית לצוותים מאפשרת יכולות לשיתופי פעולה.",
|
||||||
|
"make_org_private": "הפיכת הארגון לפרטי",
|
||||||
|
"make_org_private_description": "כשההגדרה הזו מופעלת, חברי הארגון שלך לא יוכלו לראות חברי ארגונים אחרים.",
|
||||||
"make_team_private": "להגדיר את הצוות כפרטי",
|
"make_team_private": "להגדיר את הצוות כפרטי",
|
||||||
"make_team_private_description": "כשההגדרה הזו מופעלת, חברי הצוות שלך לא יוכלו לראות חברי צוות אחרים.",
|
"make_team_private_description": "כשההגדרה הזו מופעלת, חברי הצוות שלך לא יוכלו לראות חברי צוות אחרים.",
|
||||||
"you_cannot_see_team_members": "אין לך אפשרות לראות את כל חברי הצוות של צוות פרטי.",
|
"you_cannot_see_team_members": "אין לך אפשרות לראות את כל חברי הצוות של צוות פרטי.",
|
||||||
@@ -1978,6 +1983,7 @@
|
|||||||
"connect_google_workspace": "חיבור Google Workspace",
|
"connect_google_workspace": "חיבור Google Workspace",
|
||||||
"google_workspace_admin_tooltip": "עליך/ייך להיות אדמין/ית ב-Workspace כדי להשתמש בתכונה הזו",
|
"google_workspace_admin_tooltip": "עליך/ייך להיות אדמין/ית ב-Workspace כדי להשתמש בתכונה הזו",
|
||||||
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
|
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
|
||||||
|
"create_instant_meeting_webhook_description": "אפשר ליצור את ההתליה הראשונה שלך עם ‚נוצרה פגישה מיידית’ כהזנקה לסוג אירוע זה",
|
||||||
"install_app_on": "התקנת האפליקציה ב-",
|
"install_app_on": "התקנת האפליקציה ב-",
|
||||||
"create_for": "צור/צרי עבור",
|
"create_for": "צור/צרי עבור",
|
||||||
"currency": "מטבע",
|
"currency": "מטבע",
|
||||||
@@ -2193,6 +2199,7 @@
|
|||||||
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
|
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
|
||||||
"dont_want_to_wait": "לא רוצה להמתין?",
|
"dont_want_to_wait": "לא רוצה להמתין?",
|
||||||
"meeting_started": "הפגישה החלה",
|
"meeting_started": "הפגישה החלה",
|
||||||
|
"pay_and_book": "נא לשלם כדי להזמין",
|
||||||
"booking_not_found_error": "לא ניתן למצוא הזמנה",
|
"booking_not_found_error": "לא ניתן למצוא הזמנה",
|
||||||
"booking_seats_full_error": "חלונות ההזמנה מלאים",
|
"booking_seats_full_error": "חלונות ההזמנה מלאים",
|
||||||
"missing_payment_credential_error": "פרטי התשלום חסרים",
|
"missing_payment_credential_error": "פרטי התשלום חסרים",
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
import type { GetServerSidePropsContext } from "next";
|
||||||
|
|
||||||
|
import { getServerSession } from "@calcom/features/auth/lib/getServerSession";
|
||||||
|
import { getDeploymentKey } from "@calcom/features/ee/deployment/lib/getDeploymentKey";
|
||||||
|
import prisma from "@calcom/prisma";
|
||||||
|
import { UserPermissionRole } from "@calcom/prisma/enums";
|
||||||
|
|
||||||
|
import { ssrInit } from "@server/lib/ssr";
|
||||||
|
|
||||||
|
export async function getServerSideProps(context: GetServerSidePropsContext) {
|
||||||
|
const { req } = context;
|
||||||
|
|
||||||
|
const ssr = await ssrInit(context);
|
||||||
|
const userCount = await prisma.user.count();
|
||||||
|
|
||||||
|
const session = await getServerSession({ req });
|
||||||
|
|
||||||
|
if (session?.user.role && session?.user.role !== UserPermissionRole.ADMIN) {
|
||||||
|
return {
|
||||||
|
redirect: {
|
||||||
|
destination: `/404`,
|
||||||
|
permanent: false,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const deploymentKey = await prisma.deployment.findUnique({
|
||||||
|
where: { id: 1 },
|
||||||
|
select: { licenseKey: true },
|
||||||
|
});
|
||||||
|
|
||||||
|
// Check existant CALCOM_LICENSE_KEY env var and acccount for it
|
||||||
|
if (!!process.env.CALCOM_LICENSE_KEY && !deploymentKey?.licenseKey) {
|
||||||
|
await prisma.deployment.upsert({
|
||||||
|
where: { id: 1 },
|
||||||
|
update: {
|
||||||
|
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||||
|
agreedLicenseAt: new Date(),
|
||||||
|
},
|
||||||
|
create: {
|
||||||
|
licenseKey: process.env.CALCOM_LICENSE_KEY,
|
||||||
|
agreedLicenseAt: new Date(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
const isFreeLicense = (await getDeploymentKey(prisma)) === "";
|
||||||
|
|
||||||
|
return {
|
||||||
|
props: {
|
||||||
|
trpcState: ssr.dehydrate(),
|
||||||
|
isFreeLicense,
|
||||||
|
userCount,
|
||||||
|
},
|
||||||
|
};
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import superjson from "superjson";
|
|||||||
|
|
||||||
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||||
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
import prisma, { readonlyPrisma } from "@calcom/prisma";
|
||||||
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
|
import { createServerSideHelpers } from "@calcom/trpc/react/server";
|
||||||
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
import { appRouter } from "@calcom/trpc/server/routers/_app";
|
||||||
|
|
||||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||||
@@ -26,7 +26,7 @@ export async function ssgInit<TParams extends { locale?: string }>(opts: GetStat
|
|||||||
|
|
||||||
const _i18n = await serverSideTranslations(locale, ["common"]);
|
const _i18n = await serverSideTranslations(locale, ["common"]);
|
||||||
|
|
||||||
const ssg = createProxySSGHelpers({
|
const ssg = createServerSideHelpers({
|
||||||
router: appRouter,
|
router: appRouter,
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
ctx: {
|
ctx: {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ import { forms } from "@calcom/app-store/routing-forms/trpc/procedures/forms";
|
|||||||
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
import { getLocale } from "@calcom/features/auth/lib/getLocale";
|
||||||
import { map } from "@calcom/features/flags/server/procedures/map";
|
import { map } from "@calcom/features/flags/server/procedures/map";
|
||||||
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
import { CALCOM_VERSION } from "@calcom/lib/constants";
|
||||||
import { createProxySSGHelpers } from "@calcom/trpc/react/ssg";
|
import { createServerSideHelpers } from "@calcom/trpc/react/server";
|
||||||
import { createContext } from "@calcom/trpc/server/createContext";
|
import { createContext } from "@calcom/trpc/server/createContext";
|
||||||
import { me } from "@calcom/trpc/server/routers/loggedInViewer/procedures/me";
|
import { me } from "@calcom/trpc/server/routers/loggedInViewer/procedures/me";
|
||||||
import { teamsAndUserProfilesQuery } from "@calcom/trpc/server/routers/loggedInViewer/procedures/teamsAndUserProfilesQuery";
|
import { teamsAndUserProfilesQuery } from "@calcom/trpc/server/routers/loggedInViewer/procedures/teamsAndUserProfilesQuery";
|
||||||
@@ -59,7 +59,7 @@ export async function ssrInit(context: GetServerSidePropsContext, options?: { no
|
|||||||
const locale = await getLocale(context.req);
|
const locale = await getLocale(context.req);
|
||||||
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
|
const i18n = await serverSideTranslations(locale, ["common", "vital"]);
|
||||||
|
|
||||||
const ssr = createProxySSGHelpers({
|
const ssr = createServerSideHelpers({
|
||||||
router: routerSlice,
|
router: routerSlice,
|
||||||
transformer: superjson,
|
transformer: superjson,
|
||||||
ctx: { ...ctx, locale, i18n },
|
ctx: { ...ctx, locale, i18n },
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export default function OmniInstallAppButton({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Button
|
||||||
loading={mutation.isLoading}
|
loading={mutation.isPending}
|
||||||
color="secondary"
|
color="secondary"
|
||||||
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
|
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
|
||||||
StartIcon={Plus}
|
StartIcon={Plus}
|
||||||
|
|||||||
@@ -32,64 +32,67 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
|
|||||||
AddAppMutationData,
|
AddAppMutationData,
|
||||||
Error,
|
Error,
|
||||||
{ type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean; teamId?: number } | ""
|
{ type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean; teamId?: number } | ""
|
||||||
>(async (variables) => {
|
>({
|
||||||
let type: string | null | undefined;
|
...options,
|
||||||
let isOmniInstall;
|
mutationFn: async (variables) => {
|
||||||
const teamId = variables && variables.teamId ? variables.teamId : undefined;
|
let type: string | null | undefined;
|
||||||
if (variables === "") {
|
let isOmniInstall;
|
||||||
type = _type;
|
const teamId = variables && variables.teamId ? variables.teamId : undefined;
|
||||||
} else {
|
if (variables === "") {
|
||||||
isOmniInstall = variables.isOmniInstall;
|
type = _type;
|
||||||
type = variables.type;
|
} else {
|
||||||
}
|
isOmniInstall = variables.isOmniInstall;
|
||||||
if (type?.endsWith("_other_calendar")) {
|
type = variables.type;
|
||||||
type = type.split("_other_calendar")[0];
|
}
|
||||||
}
|
if (type?.endsWith("_other_calendar")) {
|
||||||
|
type = type.split("_other_calendar")[0];
|
||||||
|
}
|
||||||
|
|
||||||
if (options?.installGoogleVideo && type !== "google_calendar")
|
if (options?.installGoogleVideo && type !== "google_calendar")
|
||||||
throw new Error("Could not install Google Meet");
|
throw new Error("Could not install Google Meet");
|
||||||
|
|
||||||
const state: IntegrationOAuthCallbackState = {
|
const state: IntegrationOAuthCallbackState = {
|
||||||
returnTo:
|
returnTo:
|
||||||
returnTo ||
|
returnTo ||
|
||||||
WEBAPP_URL +
|
WEBAPP_URL +
|
||||||
getInstalledAppPath(
|
getInstalledAppPath(
|
||||||
{ variant: variables && variables.variant, slug: variables && variables.slug },
|
{ variant: variables && variables.variant, slug: variables && variables.slug },
|
||||||
location.search
|
location.search
|
||||||
),
|
),
|
||||||
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
|
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
|
||||||
...(teamId && { teamId }),
|
...(teamId && { teamId }),
|
||||||
};
|
};
|
||||||
|
|
||||||
const stateStr = encodeURIComponent(JSON.stringify(state));
|
const stateStr = encodeURIComponent(JSON.stringify(state));
|
||||||
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
|
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
|
||||||
|
|
||||||
const res = await fetch(`/api/integrations/${type}/add${searchParams}`);
|
const res = await fetch(`/api/integrations/${type}/add${searchParams}`);
|
||||||
|
|
||||||
if (!res.ok) {
|
if (!res.ok) {
|
||||||
const errorBody = await res.json();
|
const errorBody = await res.json();
|
||||||
throw new Error(errorBody.message || "Something went wrong");
|
throw new Error(errorBody.message || "Something went wrong");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const json = await res.json();
|
||||||
|
const externalUrl = /https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin);
|
||||||
|
if (!isOmniInstall) {
|
||||||
|
gotoUrl(json.url, json.newTab);
|
||||||
|
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
||||||
|
}
|
||||||
|
|
||||||
|
// Skip redirection only if it is an OmniInstall and redirect URL isn't of some other origin
|
||||||
|
// This allows installation of apps like Stripe to still redirect to their authentication pages.
|
||||||
|
|
||||||
|
// Check first that the URL is absolute, then check that it is of different origin from the current.
|
||||||
|
if (externalUrl) {
|
||||||
|
// TODO: For Omni installation to authenticate and come back to the page where installation was initiated, some changes need to be done in all apps' add callbacks
|
||||||
|
gotoUrl(json.url, json.newTab);
|
||||||
|
return { setupPending: externalUrl };
|
||||||
|
}
|
||||||
|
|
||||||
const json = await res.json();
|
|
||||||
const externalUrl = /https?:\/\//.test(json.url) && !json.url.startsWith(window.location.origin);
|
|
||||||
if (!isOmniInstall) {
|
|
||||||
gotoUrl(json.url, json.newTab);
|
|
||||||
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
||||||
}
|
},
|
||||||
|
});
|
||||||
// Skip redirection only if it is an OmniInstall and redirect URL isn't of some other origin
|
|
||||||
// This allows installation of apps like Stripe to still redirect to their authentication pages.
|
|
||||||
|
|
||||||
// Check first that the URL is absolute, then check that it is of different origin from the current.
|
|
||||||
if (externalUrl) {
|
|
||||||
// TODO: For Omni installation to authenticate and come back to the page where installation was initiated, some changes need to be done in all apps' add callbacks
|
|
||||||
gotoUrl(json.url, json.newTab);
|
|
||||||
return { setupPending: externalUrl };
|
|
||||||
}
|
|
||||||
|
|
||||||
return { setupPending: externalUrl || json.url.endsWith("/setup") };
|
|
||||||
}, options);
|
|
||||||
|
|
||||||
return mutation;
|
return mutation;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -121,7 +121,7 @@ function AlbySetupPage(props: IAlbySetupProps) {
|
|||||||
});
|
});
|
||||||
}, [credentialId, props.clientId, props.clientSecret, saveKeysMutation]);
|
}, [credentialId, props.clientId, props.clientSecret, saveKeysMutation]);
|
||||||
|
|
||||||
if (integrations.isLoading) {
|
if (integrations.isPending) {
|
||||||
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,22 +15,26 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
|
|||||||
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
|
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
|
||||||
const { data } = trpc.viewer.appBasecamp3.projects.useQuery();
|
const { data } = trpc.viewer.appBasecamp3.projects.useQuery();
|
||||||
const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation();
|
const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation();
|
||||||
useEffect(() => {
|
|
||||||
setSelectedProject({
|
useEffect(
|
||||||
value: data?.projects.currentProject,
|
function refactorMeWithoutEffect() {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
setSelectedProject({
|
||||||
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
|
value: data?.projects.currentProject,
|
||||||
});
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
setProjects(
|
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
});
|
||||||
data?.projects?.map((project: any) => {
|
setProjects(
|
||||||
return {
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
value: project.id,
|
data?.projects?.map((project: any) => {
|
||||||
label: project.name,
|
return {
|
||||||
};
|
value: project.id,
|
||||||
})
|
label: project.name,
|
||||||
);
|
};
|
||||||
}, [data]);
|
})
|
||||||
|
);
|
||||||
|
},
|
||||||
|
[data]
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<AppCard
|
<AppCard
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ export const InstallAppButtonWithoutPlanCheck = (
|
|||||||
onClick: () => {
|
onClick: () => {
|
||||||
mutation.mutate({ type: props.type });
|
mutation.mutate({ type: props.type });
|
||||||
},
|
},
|
||||||
loading: mutation.isLoading,
|
loading: mutation.isPending,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
@@ -56,10 +56,10 @@ export const InstallAppButton = (
|
|||||||
disableInstall?: boolean;
|
disableInstall?: boolean;
|
||||||
} & InstallAppButtonProps
|
} & InstallAppButtonProps
|
||||||
) => {
|
) => {
|
||||||
const { isLoading: isUserLoading, data: user } = trpc.viewer.me.useQuery();
|
const { isPending: isUserLoading, data: user } = trpc.viewer.me.useQuery();
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const proProtectionElementRef = useRef<HTMLDivElement | null>(null);
|
const proProtectionElementRef = useRef<HTMLDivElement | null>(null);
|
||||||
const { isLoading: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
|
const { isPending: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const el = proProtectionElementRef.current;
|
const el = proProtectionElementRef.current;
|
||||||
|
|||||||
@@ -1,18 +1,14 @@
|
|||||||
---
|
---
|
||||||
items:
|
items:
|
||||||
- 1.png
|
- 1.jpeg
|
||||||
- 2.png
|
- 2.png
|
||||||
- 3.png
|
- 3.png
|
||||||
- 4.png
|
- 4.png
|
||||||
- 5.png
|
|
||||||
---
|
---
|
||||||
|
|
||||||
{DESCRIPTION}
|
### It’s time.
|
||||||
|
|
||||||
### Cron's most valuable features
|
All of your commitments, now in one place. Meet the beautifully designed, fully integrated calendar for your work and life.
|
||||||
|
|
||||||
<!-- From "FAQs: What are Cron’s most valuable features?"
|
|
||||||
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
|
|
||||||
|
|
||||||
- **Availability sharing** — mark holds on the grid, share the slots, and schedule with ease.
|
- **Availability sharing** — mark holds on the grid, share the slots, and schedule with ease.
|
||||||
- **Team overlays** — quickly overlay teammates' calendars.
|
- **Team overlays** — quickly overlay teammates' calendars.
|
||||||
|
|||||||
@@ -1,15 +1,15 @@
|
|||||||
{
|
{
|
||||||
"/*": "Don't modify slug - If required, do it using cli edit command",
|
"/*": "Don't modify slug - If required, do it using cli edit command",
|
||||||
"name": "Cron",
|
"name": "Notion Calendar",
|
||||||
"slug": "cron",
|
"slug": "cron",
|
||||||
"type": "cron_other",
|
"type": "cron_calendar",
|
||||||
"logo": "logo.png",
|
"logo": "logo.png",
|
||||||
"url": "https://github.com/miguelnietoa",
|
"url": "https://github.com/miguelnietoa",
|
||||||
"variant": "other",
|
"variant": "calendar",
|
||||||
"categories": ["calendar"],
|
"categories": ["calendar"],
|
||||||
"publisher": "Miguel Nieto",
|
"publisher": "Miguel Nieto",
|
||||||
"email": "support@cal.com",
|
"email": "support@cal.com",
|
||||||
"description": "Cron is the next-generation calendar for professionals and teams.",
|
"description": "All of your commitments, now in one place.",
|
||||||
"isTemplate": false,
|
"isTemplate": false,
|
||||||
"__createdUsingCli": true,
|
"__createdUsingCli": true,
|
||||||
"__template": "link-as-an-app",
|
"__template": "link-as-an-app",
|
||||||
|
|||||||
@@ -10,5 +10,5 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@calcom/types": "*"
|
"@calcom/types": "*"
|
||||||
},
|
},
|
||||||
"description": "Cron is the next-generation calendar for professionals and teams."
|
"description": "All of your commitments, now in one place."
|
||||||
}
|
}
|
||||||
|
After Width: | Height: | Size: 173 KiB |
|
Before Width: | Height: | Size: 367 KiB |
|
Before Width: | Height: | Size: 487 KiB After Width: | Height: | Size: 477 KiB |
|
Before Width: | Height: | Size: 1000 KiB After Width: | Height: | Size: 131 KiB |
|
Before Width: | Height: | Size: 182 KiB After Width: | Height: | Size: 114 KiB |
|
Before Width: | Height: | Size: 73 KiB |
|
Before Width: | Height: | Size: 36 KiB After Width: | Height: | Size: 61 KiB |
@@ -11,7 +11,7 @@ export default function InstallAppButton(props: InstallAppButtonProps) {
|
|||||||
onClick() {
|
onClick() {
|
||||||
mutation.mutate("");
|
mutation.mutate("");
|
||||||
},
|
},
|
||||||
loading: mutation.isLoading,
|
loading: mutation.isPending,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ export default function InstallAppButton(props: InstallAppButtonProps) {
|
|||||||
onClick() {
|
onClick() {
|
||||||
mutation.mutate("");
|
mutation.mutate("");
|
||||||
},
|
},
|
||||||
loading: mutation.isLoading,
|
loading: mutation.isPending,
|
||||||
})}
|
})}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -23,15 +23,15 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||||||
const [nextOffset, setNextOffset] = useState<number>(0);
|
const [nextOffset, setNextOffset] = useState<number>(0);
|
||||||
const [keyword, setKeyword] = useState<string>("");
|
const [keyword, setKeyword] = useState<string>("");
|
||||||
const { isOpenDialog, setIsOpenDialog } = props;
|
const { isOpenDialog, setIsOpenDialog } = props;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const [errorMessage, setErrorMessage] = useState("");
|
const [errorMessage, setErrorMessage] = useState("");
|
||||||
const [selectedMode, setSelectedMode] = useState<Mode>(MODE_SEARCH);
|
const [selectedMode, setSelectedMode] = useState<Mode>(MODE_SEARCH);
|
||||||
|
|
||||||
const searchGiphy = async (keyword: string, offset: number) => {
|
const searchGiphy = async (keyword: string, offset: number) => {
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
const res = await fetch("/api/integrations/giphy/search", {
|
const res = await fetch("/api/integrations/giphy/search", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -53,15 +53,15 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||||||
setErrorMessage("No Result found");
|
setErrorMessage("No Result found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsPending(false);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const getGiphyByUrl = async (url: string) => {
|
const getGiphyByUrl = async (url: string) => {
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
setErrorMessage("");
|
setErrorMessage("");
|
||||||
const res = await fetch("/api/integrations/giphy/get", {
|
const res = await fetch("/api/integrations/giphy/get", {
|
||||||
method: "POST",
|
method: "POST",
|
||||||
@@ -81,7 +81,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||||||
setErrorMessage("No Result found");
|
setErrorMessage("No Result found");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsPending(false);
|
||||||
return null;
|
return null;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -139,14 +139,14 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<Button type="submit" tabIndex={-1} color="secondary" loading={isLoading}>
|
<Button type="submit" tabIndex={-1} color="secondary" loading={isPending}>
|
||||||
{t("search")}
|
{t("search")}
|
||||||
</Button>
|
</Button>
|
||||||
</form>
|
</form>
|
||||||
{gifImage && (
|
{gifImage && (
|
||||||
<div className="flex flex-col items-center space-x-2 space-y-2 pt-3 rtl:space-x-reverse">
|
<div className="flex flex-col items-center space-x-2 space-y-2 pt-3 rtl:space-x-reverse">
|
||||||
<div className="bg-subtle flex w-full items-center justify-center">
|
<div className="bg-subtle flex w-full items-center justify-center">
|
||||||
{isLoading ? (
|
{isPending ? (
|
||||||
<div className="flex h-[200px] w-full items-center justify-center bg-gray-400 pb-3 pt-3">
|
<div className="flex h-[200px] w-full items-center justify-center bg-gray-400 pb-3 pt-3">
|
||||||
<svg
|
<svg
|
||||||
className={classNames("mx-4 h-5 w-5 animate-spin", "text-inverted dark:text-emphasis")}
|
className={classNames("mx-4 h-5 w-5 animate-spin", "text-inverted dark:text-emphasis")}
|
||||||
@@ -182,7 +182,7 @@ export const SearchDialog = (props: ISearchDialog) => {
|
|||||||
size="sm"
|
size="sm"
|
||||||
color="secondary"
|
color="secondary"
|
||||||
type="button"
|
type="button"
|
||||||
loading={isLoading}
|
loading={isPending}
|
||||||
onClick={() => searchGiphy(keyword, nextOffset)}>
|
onClick={() => searchGiphy(keyword, nextOffset)}>
|
||||||
Shuffle
|
Shuffle
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@@ -59,7 +59,7 @@ export default function MakeSetup({ inviteLink }: InferGetServerSidePropsType<ty
|
|||||||
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
|
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (integrations.isLoading) {
|
if (integrations.isPending) {
|
||||||
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
|
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export default function PayPalSetup() {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (integrations.isLoading) {
|
if (integrations.isPending) {
|
||||||
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
return <div className="absolute z-50 flex h-screen w-full items-center bg-gray-200" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -147,7 +147,7 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
|
|||||||
</div>
|
</div>
|
||||||
<DialogFooter showDivider className="mt-12">
|
<DialogFooter showDivider className="mt-12">
|
||||||
<DialogClose />
|
<DialogClose />
|
||||||
<Button loading={mutation.isLoading} data-testid="add-form" type="submit">
|
<Button loading={mutation.isPending} data-testid="add-form" type="submit">
|
||||||
{t("continue")}
|
{t("continue")}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
@@ -234,7 +234,7 @@ function Dialogs({
|
|||||||
<RoutingFormEmbedDialog />
|
<RoutingFormEmbedDialog />
|
||||||
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
|
||||||
<ConfirmationDialogContent
|
<ConfirmationDialogContent
|
||||||
isLoading={deleteMutation.isLoading}
|
isPending={deleteMutation.isPending}
|
||||||
variety="danger"
|
variety="danger"
|
||||||
title={t("delete_form")}
|
title={t("delete_form")}
|
||||||
confirmBtnText={t("delete_form_action")}
|
confirmBtnText={t("delete_form_action")}
|
||||||
@@ -264,12 +264,12 @@ const actionsCtx = createContext({
|
|||||||
_delete: {
|
_delete: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||||
onAction: (_arg: { routingForm: RoutingForm | null }) => {},
|
onAction: (_arg: { routingForm: RoutingForm | null }) => {},
|
||||||
isLoading: false,
|
isPending: false,
|
||||||
},
|
},
|
||||||
toggle: {
|
toggle: {
|
||||||
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
|
||||||
onAction: (_arg: { routingForm: RoutingForm | null; checked: boolean }) => {},
|
onAction: (_arg: { routingForm: RoutingForm | null; checked: boolean }) => {},
|
||||||
isLoading: false,
|
isPending: false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -332,7 +332,7 @@ export function FormActionsProvider({ appUrl, children }: { appUrl: string; chil
|
|||||||
setDeleteDialogOpen(true);
|
setDeleteDialogOpen(true);
|
||||||
setDeleteDialogFormId(routingForm.id);
|
setDeleteDialogFormId(routingForm.id);
|
||||||
},
|
},
|
||||||
isLoading: false,
|
isPending: false,
|
||||||
},
|
},
|
||||||
toggle: {
|
toggle: {
|
||||||
onAction: ({ routingForm, checked }) => {
|
onAction: ({ routingForm, checked }) => {
|
||||||
@@ -344,7 +344,7 @@ export function FormActionsProvider({ appUrl, children }: { appUrl: string; chil
|
|||||||
disabled: !checked,
|
disabled: !checked,
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
isLoading: toggleMutation.isLoading,
|
isPending: toggleMutation.isPending,
|
||||||
},
|
},
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
@@ -446,7 +446,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
|||||||
},
|
},
|
||||||
_delete: {
|
_delete: {
|
||||||
onClick: () => _delete.onAction({ routingForm }),
|
onClick: () => _delete.onAction({ routingForm }),
|
||||||
loading: _delete.isLoading,
|
loading: _delete.isPending,
|
||||||
},
|
},
|
||||||
create: {
|
create: {
|
||||||
onClick: () => openModal({ action: "new", target: "" }),
|
onClick: () => openModal({ action: "new", target: "" }),
|
||||||
@@ -479,7 +479,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
loading: toggle.isLoading,
|
loading: toggle.isPending,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -65,7 +65,7 @@ const Actions = ({
|
|||||||
}: {
|
}: {
|
||||||
form: RoutingFormWithResponseCount;
|
form: RoutingFormWithResponseCount;
|
||||||
mutation: {
|
mutation: {
|
||||||
isLoading: boolean;
|
isPending: boolean;
|
||||||
};
|
};
|
||||||
}) => {
|
}) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
@@ -218,7 +218,7 @@ const Actions = ({
|
|||||||
</FormActionsDropdown>
|
</FormActionsDropdown>
|
||||||
</div>
|
</div>
|
||||||
<VerticalDivider />
|
<VerticalDivider />
|
||||||
<Button data-testid="update-form" loading={mutation.isLoading} type="submit" color="primary">
|
<Button data-testid="update-form" loading={mutation.isPending} type="submit" color="primary">
|
||||||
{t("save")}
|
{t("save")}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
@@ -518,7 +518,7 @@ function SingleForm({ form, appUrl, Page }: SingleFormComponentProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export default function SingleFormWrapper({ form: _form, ...props }: SingleFormComponentProps) {
|
export default function SingleFormWrapper({ form: _form, ...props }: SingleFormComponentProps) {
|
||||||
const { data: form, isLoading } = trpc.viewer.appRoutingForms.formQuery.useQuery(
|
const { data: form, isPending } = trpc.viewer.appRoutingForms.formQuery.useQuery(
|
||||||
{ id: _form.id },
|
{ id: _form.id },
|
||||||
{
|
{
|
||||||
initialData: _form,
|
initialData: _form,
|
||||||
@@ -527,7 +527,7 @@ export default function SingleFormWrapper({ form: _form, ...props }: SingleFormC
|
|||||||
);
|
);
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
if (isLoading) {
|
if (isPending) {
|
||||||
// It shouldn't be possible because we are passing the data from SSR to it as initialData. So, no need for skeleton here
|
// It shouldn't be possible because we are passing the data from SSR to it as initialData. So, no need for skeleton here
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -34,7 +34,7 @@ type QueryBuilderUpdatedConfig = typeof QueryBuilderInitialConfig & { fields: Co
|
|||||||
const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: JsonLogicQuery | null }) => {
|
const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: JsonLogicQuery | null }) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
|
|
||||||
const { isLoading, status, data, isFetching, error, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
const { isPending, status, data, isFetching, error, isFetchingNextPage, hasNextPage, fetchNextPage } =
|
||||||
trpc.viewer.appRoutingForms.report.useInfiniteQuery(
|
trpc.viewer.appRoutingForms.report.useInfiniteQuery(
|
||||||
{
|
{
|
||||||
formId: formId,
|
formId: formId,
|
||||||
@@ -57,7 +57,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
|
|||||||
|
|
||||||
const headers = useRef<string[] | null>(null);
|
const headers = useRef<string[] | null>(null);
|
||||||
|
|
||||||
if (!isLoading && !data) {
|
if (!isPending && !data) {
|
||||||
return <div>Error loading report {error?.message} </div>;
|
return <div>Error loading report {error?.message} </div>;
|
||||||
}
|
}
|
||||||
headers.current = (data?.pages && data?.pages[0]?.headers) || headers.current;
|
headers.current = (data?.pages && data?.pages[0]?.headers) || headers.current;
|
||||||
@@ -81,7 +81,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
|
|||||||
</th>
|
</th>
|
||||||
))}
|
))}
|
||||||
</tr>
|
</tr>
|
||||||
{!isLoading &&
|
{!isPending &&
|
||||||
data?.pages.map((page) => {
|
data?.pages.map((page) => {
|
||||||
return page.responses?.map((responses, rowIndex) => {
|
return page.responses?.map((responses, rowIndex) => {
|
||||||
const isLastRow = page.responses.length - 1 === rowIndex;
|
const isLastRow = page.responses.length - 1 === rowIndex;
|
||||||
@@ -113,7 +113,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
|
|||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
</table>
|
</table>
|
||||||
{isLoading ? <div className="text-default p-2">{t("loading")}</div> : ""}
|
{isPending ? <div className="text-default p-2">{t("loading")}</div> : ""}
|
||||||
{hasNextPage && (
|
{hasNextPage && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -153,7 +153,7 @@ function RoutingForm({ form, profile, ...restProps }: Props) {
|
|||||||
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
<div className="mt-4 flex justify-end space-x-2 rtl:space-x-reverse">
|
||||||
<Button
|
<Button
|
||||||
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast dark:border-transparent"
|
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast dark:border-transparent"
|
||||||
loading={responseMutation.isLoading}
|
loading={responseMutation.isPending}
|
||||||
type="submit"
|
type="submit"
|
||||||
color="primary">
|
color="primary">
|
||||||
{t("submit")}
|
{t("submit")}
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export interface InstallAppButtonProps {
|
|||||||
renderProps: ButtonProps & {
|
renderProps: ButtonProps & {
|
||||||
/** Tells that the default render component should be used */
|
/** Tells that the default render component should be used */
|
||||||
useDefaultComponent?: boolean;
|
useDefaultComponent?: boolean;
|
||||||
isLoading?: boolean;
|
isPending?: boolean;
|
||||||
}
|
}
|
||||||
) => JSX.Element;
|
) => JSX.Element;
|
||||||
onChanged?: () => unknown;
|
onChanged?: () => unknown;
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const wipeMyCalAction = async (props: IWipeMyCalAction) => {
|
|||||||
export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const { isOpenDialog, setIsOpenDialog } = props;
|
const { isOpenDialog, setIsOpenDialog } = props;
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isPending, setIsPending] = useState(false);
|
||||||
const today = dayjs();
|
const today = dayjs();
|
||||||
const initialDate = today.startOf("day");
|
const initialDate = today.startOf("day");
|
||||||
const endDate = today.endOf("day");
|
const endDate = today.endOf("day");
|
||||||
@@ -52,9 +52,9 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
|||||||
|
|
||||||
const utils = trpc.useContext();
|
const utils = trpc.useContext();
|
||||||
|
|
||||||
const rescheduleApi = useMutation(
|
const rescheduleApi = useMutation({
|
||||||
async () => {
|
mutationFn: async () => {
|
||||||
setIsLoading(true);
|
setIsPending(true);
|
||||||
try {
|
try {
|
||||||
const result = await wipeMyCalAction({
|
const result = await wipeMyCalAction({
|
||||||
initialDate: initialDate.toISOString(),
|
initialDate: initialDate.toISOString(),
|
||||||
@@ -68,14 +68,12 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
|||||||
showToast(t("unexpected_error_try_again"), "error");
|
showToast(t("unexpected_error_try_again"), "error");
|
||||||
// @TODO: notify
|
// @TODO: notify
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
setIsPending(false);
|
||||||
},
|
},
|
||||||
{
|
async onSettled() {
|
||||||
async onSettled() {
|
await utils.viewer.bookings.invalidate();
|
||||||
await utils.viewer.bookings.invalidate();
|
},
|
||||||
},
|
});
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
|
||||||
@@ -104,7 +102,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
|
|||||||
<Button
|
<Button
|
||||||
color="primary"
|
color="primary"
|
||||||
data-testid="send_request"
|
data-testid="send_request"
|
||||||
disabled={isLoading}
|
disabled={isPending}
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
try {
|
try {
|
||||||
rescheduleApi.mutate();
|
rescheduleApi.mutate();
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ interface IWipeMyCalActionButtonProps {
|
|||||||
const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
|
const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
|
||||||
const { bookingsEmpty, bookingStatus } = props;
|
const { bookingsEmpty, bookingStatus } = props;
|
||||||
const [openDialog, setOpenDialog] = useState(false);
|
const [openDialog, setOpenDialog] = useState(false);
|
||||||
const { isSuccess, isLoading, data } = trpc.viewer.integrations.useQuery({
|
const { isSuccess, isPending, data } = trpc.viewer.integrations.useQuery({
|
||||||
variant: "other",
|
variant: "other",
|
||||||
onlyInstalled: undefined,
|
onlyInstalled: undefined,
|
||||||
});
|
});
|
||||||
@@ -27,7 +27,7 @@ const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
{data && isSuccess && !isLoading && credentialId && (
|
{data && isSuccess && !isPending && credentialId && (
|
||||||
<div className="mb-4">
|
<div className="mb-4">
|
||||||
<ConfirmDialog isOpenDialog={openDialog} setIsOpenDialog={setOpenDialog} />
|
<ConfirmDialog isOpenDialog={openDialog} setIsOpenDialog={setOpenDialog} />
|
||||||
<Button color="primary" onClick={() => setOpenDialog(true)} data-testid="wipe-today-button">
|
<Button color="primary" onClick={() => setOpenDialog(true)} data-testid="wipe-today-button">
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ export default function ZapierSetup(props: IZapierSetupProps) {
|
|||||||
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
|
setNewApiKeys({ ...newApiKeys, [teamId || ""]: apiKey });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (integrations.isLoading) {
|
if (integrations.isPending) {
|
||||||
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
|
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -94,12 +94,12 @@ const _getEventType = async (id: number) => {
|
|||||||
|
|
||||||
type EventType = Awaited<ReturnType<typeof getEventType>>;
|
type EventType = Awaited<ReturnType<typeof getEventType>>;
|
||||||
|
|
||||||
const getUser = (...args: Parameters<typeof _getUser>): ReturnType<typeof _getUser> => {
|
const getUser = async (...args: Parameters<typeof _getUser>): Promise<ReturnType<typeof _getUser>> => {
|
||||||
return monitorCallbackSync(_getUser, ...args);
|
return monitorCallbackAsync(_getUser, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getUser = (where: Prisma.UserWhereInput) =>
|
const _getUser = async (where: Prisma.UserWhereInput) => {
|
||||||
prisma.user.findFirst({
|
return await prisma.user.findFirst({
|
||||||
where,
|
where,
|
||||||
select: {
|
select: {
|
||||||
...availabilityUserSelect,
|
...availabilityUserSelect,
|
||||||
@@ -108,17 +108,18 @@ const _getUser = (where: Prisma.UserWhereInput) =>
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
type User = Awaited<ReturnType<typeof getUser>>;
|
type User = Awaited<ReturnType<typeof getUser>>;
|
||||||
|
|
||||||
export const getCurrentSeats = (
|
export const getCurrentSeats = async (
|
||||||
...args: Parameters<typeof _getCurrentSeats>
|
...args: Parameters<typeof _getCurrentSeats>
|
||||||
): ReturnType<typeof _getCurrentSeats> => {
|
): Promise<ReturnType<typeof _getCurrentSeats>> => {
|
||||||
return monitorCallbackSync(_getCurrentSeats, ...args);
|
return monitorCallbackAsync(_getCurrentSeats, ...args);
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =>
|
const _getCurrentSeats = async (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) => {
|
||||||
prisma.booking.findMany({
|
return await prisma.booking.findMany({
|
||||||
where: {
|
where: {
|
||||||
eventTypeId,
|
eventTypeId,
|
||||||
startTime: {
|
startTime: {
|
||||||
@@ -137,6 +138,7 @@ const _getCurrentSeats = (eventTypeId: number, dateFrom: Dayjs, dateTo: Dayjs) =
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
export type CurrentSeats = Awaited<ReturnType<typeof getCurrentSeats>>;
|
||||||
|
|
||||||
|
|||||||
@@ -270,7 +270,7 @@ const AdminAppsListContainer = () => {
|
|||||||
const { t } = useLocale();
|
const { t } = useLocale();
|
||||||
const category = searchParams?.get("category") || AppCategories.calendar;
|
const category = searchParams?.get("category") || AppCategories.calendar;
|
||||||
|
|
||||||
const { data: apps, isLoading } = trpc.viewer.appsRouter.listLocal.useQuery(
|
const { data: apps, isPending } = trpc.viewer.appsRouter.listLocal.useQuery(
|
||||||
{ category },
|
{ category },
|
||||||
{ enabled: searchParams !== null }
|
{ enabled: searchParams !== null }
|
||||||
);
|
);
|
||||||
@@ -291,7 +291,7 @@ const AdminAppsListContainer = () => {
|
|||||||
|
|
||||||
const handleModelOpen = (data: EditModalState) => setModalState({ ...data });
|
const handleModelOpen = (data: EditModalState) => setModalState({ ...data });
|
||||||
|
|
||||||
if (isLoading) return <SkeletonLoader />;
|
if (isPending) return <SkeletonLoader />;
|
||||||
|
|
||||||
if (!apps || apps.length === 0) {
|
if (!apps || apps.length === 0) {
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
|
|||||||
import type { NextApiRequest } from "next";
|
import type { NextApiRequest } from "next";
|
||||||
|
|
||||||
import prisma from "@calcom/prisma";
|
import prisma from "@calcom/prisma";
|
||||||
import type { OAuthTokenPayload } from "@calcom/web/pages/api/auth/oauth/token";
|
import type { OAuthTokenPayload } from "@calcom/types/oauth";
|
||||||
|
|
||||||
export default async function isAuthorized(req: NextApiRequest, requiredScopes: string[] = []) {
|
export default async function isAuthorized(req: NextApiRequest, requiredScopes: string[] = []) {
|
||||||
const token = req.headers.authorization?.split(" ")[1] || "";
|
const token = req.headers.authorization?.split(" ")[1] || "";
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import { LazyMotion, m, AnimatePresence } from "framer-motion";
|
import { LazyMotion, m, AnimatePresence } from "framer-motion";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { useSearchParams } from "next/navigation";
|
import { useSearchParams } from "next/navigation";
|
||||||
import { usePathname, useRouter } from "next/navigation";
|
import { usePathname, useRouter } from "next/navigation";
|
||||||
@@ -70,6 +71,7 @@ const BookerComponent = ({
|
|||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const event = useEvent();
|
const event = useEvent();
|
||||||
|
const session = useSession();
|
||||||
const { selectedTimeslot, setSelectedTimeslot } = useSlots(event);
|
const { selectedTimeslot, setSelectedTimeslot } = useSlots(event);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
@@ -207,7 +209,7 @@ const BookerComponent = ({
|
|||||||
} = useCalendars();
|
} = useCalendars();
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (event.isLoading) return setBookerState("loading");
|
if (event.isPending) return setBookerState("loading");
|
||||||
if (!selectedDate) return setBookerState("selecting_date");
|
if (!selectedDate) return setBookerState("selecting_date");
|
||||||
if (!selectedTimeslot) return setBookerState("selecting_time");
|
if (!selectedTimeslot) return setBookerState("selecting_time");
|
||||||
return setBookerState("booking");
|
return setBookerState("booking");
|
||||||
@@ -362,7 +364,7 @@ const BookerComponent = ({
|
|||||||
"bg-default dark:bg-muted sticky top-0 z-10"
|
"bg-default dark:bg-muted sticky top-0 z-10"
|
||||||
)}>
|
)}>
|
||||||
<Header
|
<Header
|
||||||
isMyLink={Boolean(username === event?.data?.owner?.username)}
|
isMyLink={Boolean(username === session?.data?.user.username)}
|
||||||
eventSlug={eventSlug}
|
eventSlug={eventSlug}
|
||||||
enabledLayouts={bookerLayouts.enabledLayouts}
|
enabledLayouts={bookerLayouts.enabledLayouts}
|
||||||
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
extraDays={layout === BookerLayouts.COLUMN_VIEW ? columnViewExtraDays.current : extraDays}
|
||||||
@@ -399,7 +401,7 @@ const BookerComponent = ({
|
|||||||
<BookerSection
|
<BookerSection
|
||||||
area="meta"
|
area="meta"
|
||||||
className="max-w-screen flex w-full flex-col md:w-[var(--booker-meta-width)]">
|
className="max-w-screen flex w-full flex-col md:w-[var(--booker-meta-width)]">
|
||||||
<EventMeta event={event.data} isLoading={event.isLoading} />
|
<EventMeta event={event.data} isPending={event.isPending} />
|
||||||
{layout !== BookerLayouts.MONTH_VIEW &&
|
{layout !== BookerLayouts.MONTH_VIEW &&
|
||||||
!(layout === "mobile" && bookerState === "booking") && (
|
!(layout === "mobile" && bookerState === "booking") && (
|
||||||
<div className="mt-auto px-5 py-3 ">
|
<div className="mt-auto px-5 py-3 ">
|
||||||
@@ -434,7 +436,7 @@ const BookerComponent = ({
|
|||||||
visible={layout === BookerLayouts.WEEK_VIEW}
|
visible={layout === BookerLayouts.WEEK_VIEW}
|
||||||
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
|
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
|
||||||
{...fadeInLeft}>
|
{...fadeInLeft}>
|
||||||
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isLoading} />
|
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isPending} />
|
||||||
</BookerSection>
|
</BookerSection>
|
||||||
|
|
||||||
<BookerSection
|
<BookerSection
|
||||||
@@ -456,7 +458,7 @@ const BookerComponent = ({
|
|||||||
extraDays={extraDays}
|
extraDays={extraDays}
|
||||||
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
limitHeight={layout === BookerLayouts.MONTH_VIEW}
|
||||||
schedule={schedule?.data}
|
schedule={schedule?.data}
|
||||||
isLoading={schedule.isLoading}
|
isLoading={schedule.isPending}
|
||||||
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
|
||||||
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
|
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ export const BookEventForm = ({
|
|||||||
}, [eventType]);
|
}, [eventType]);
|
||||||
|
|
||||||
if (eventQuery.isError) return <Alert severity="warning" message={t("error_booking_event")} />;
|
if (eventQuery.isError) return <Alert severity="warning" message={t("error_booking_event")} />;
|
||||||
if (eventQuery.isLoading || !eventQuery.data) return <FormSkeleton />;
|
if (eventQuery.isPending || !eventQuery.data) return <FormSkeleton />;
|
||||||
if (!timeslot)
|
if (!timeslot)
|
||||||
return (
|
return (
|
||||||
<EmptyScreen
|
<EmptyScreen
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ export const DatePicker = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<DatePickerComponent
|
<DatePickerComponent
|
||||||
isLoading={schedule.isLoading}
|
isPending={schedule.isPending}
|
||||||
onChange={(date: Dayjs | null) => {
|
onChange={(date: Dayjs | null) => {
|
||||||
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
|
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
|
||||||
}}
|
}}
|
||||||
|
|||||||
@@ -24,10 +24,10 @@ const TimezoneSelect = dynamic(
|
|||||||
|
|
||||||
export const EventMeta = ({
|
export const EventMeta = ({
|
||||||
event,
|
event,
|
||||||
isLoading,
|
isPending,
|
||||||
}: {
|
}: {
|
||||||
event: useEventReturnType["data"];
|
event: useEventReturnType["data"];
|
||||||
isLoading: useEventReturnType["isLoading"];
|
isPending: useEventReturnType["isPending"];
|
||||||
}) => {
|
}) => {
|
||||||
const { setTimezone, timeFormat, timezone } = useTimePreferences();
|
const { setTimezone, timeFormat, timezone } = useTimePreferences();
|
||||||
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
const selectedDuration = useBookerStore((state) => state.selectedDuration);
|
||||||
@@ -40,7 +40,6 @@ export const EventMeta = ({
|
|||||||
shallow
|
shallow
|
||||||
);
|
);
|
||||||
const { i18n, t } = useLocale();
|
const { i18n, t } = useLocale();
|
||||||
|
|
||||||
const embedUiConfig = useEmbedUiConfig();
|
const embedUiConfig = useEmbedUiConfig();
|
||||||
const isEmbed = useIsEmbed();
|
const isEmbed = useIsEmbed();
|
||||||
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
|
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
|
||||||
@@ -66,12 +65,12 @@ export const EventMeta = ({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="relative z-10 p-6" data-testid="event-meta">
|
<div className="relative z-10 p-6" data-testid="event-meta">
|
||||||
{isLoading && (
|
{isPending && (
|
||||||
<m.div {...fadeInUp} initial="visible" layout>
|
<m.div {...fadeInUp} initial="visible" layout>
|
||||||
<EventMetaSkeleton />
|
<EventMetaSkeleton />
|
||||||
</m.div>
|
</m.div>
|
||||||
)}
|
)}
|
||||||
{!isLoading && !!event && (
|
{!isPending && !!event && (
|
||||||
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
|
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
|
||||||
<EventMembers
|
<EventMembers
|
||||||
schedulingType={event.schedulingType}
|
schedulingType={event.schedulingType}
|
||||||
|
|||||||
@@ -1,8 +1,7 @@
|
|||||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||||
import { SchedulingType } from "@calcom/prisma/enums";
|
import { SchedulingType } from "@calcom/prisma/enums";
|
||||||
import { type RouterOutputs } from "@calcom/trpc";
|
import { type RouterOutputs } from "@calcom/trpc";
|
||||||
import { Button } from "@calcom/ui";
|
import { Button, UserAvatarGroupWithOrg } from "@calcom/ui";
|
||||||
import { UserAvatarGroupWithOrg } from "@calcom/web/components/ui/avatar/UserAvatarGroupWithOrg";
|
|
||||||
|
|
||||||
interface IInstantBookingProps {
|
interface IInstantBookingProps {
|
||||||
onConnectNow: () => void;
|
onConnectNow: () => void;
|
||||||
|
|||||||