Compare commits

...
14 Commits
Author SHA1 Message Date
Keith Williams 0860036607 v3.7.7
release.yml / release (push) Canceled after 0s
2024-01-24 11:06:38 -03:00
Peer RichelsenandOmar López d080a0ef2d feat: notion calendar (#13299)
* replaced cron with notion calendar

* fixed app store for cron

* fixed slug

* Apply suggestions from code review

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-24 12:10:15 +00:00
a6eab74fd1 feat: make org members list private (#13373)
* feat: make org members list private

* fix: add isOrg to MakeTeamPrivateSwitch in other-team-members-view

---------

Co-authored-by: supalarry <lauris.skraucis@gmail.com>
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
2024-01-24 11:28:34 +00:00
0a2e275908 fix: UI mismatch with form state (#10651)
* fix: UI mismatch with form state

* Fix: Sync form state with ui for more fields & slight code refactor

* Ensure Stripe app is enabled to disable seats

---------

Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Hariom <hariombalhara@gmail.com>
Co-authored-by: Joe Au-Yeung <65426560+joeauyeung@users.noreply.github.com>
Co-authored-by: Joe Au-Yeung <j.auyeung419@gmail.com>
2024-01-24 10:55:31 +00:00
Crowdin Bot 3b05ebc5b5 New Crowdin translations by Github Action 2024-01-24 10:46:13 +00:00
Omar López e5b4d2fb80 fix: API slots endpoint return format (#13377) 2024-01-24 10:43:13 +00:00
Syed Ali Shahbaz 00a2a3d8a8 Fix slot wrapper (#13379) 2024-01-24 07:01:57 +00:00
30ba168fc3 chore: bump @trpc/* and @tanstack/react-query (#13335)
Co-authored-by: Peer Richelsen <peeroke@gmail.com>
Co-authored-by: Omar López <zomars@me.com>
Co-authored-by: CarinaWolli <wollencarina@gmail.com>
Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: zomars <zomars@me.com>
Co-authored-by: Udit Takkar <53316345+Udit-takkar@users.noreply.github.com>
Co-authored-by: Benny Joo <sldisek783@gmail.com>
Co-authored-by: Carina Wollendorfer <30310907+CarinaWolli@users.noreply.github.com>
Co-authored-by: Gergő Móricz <mo.geryy@gmail.com>
Co-authored-by: Keith Williams <keithwillcode@gmail.com>
2024-01-23 16:42:29 -07:00
SashaandDmytro Hryshyn 12eec186b2 chore: [app-router-migration 31] migrate the "/auth/setup" page (#13196)
* wip

* chore: setup page migration

* fix: conventions

* fix: convention

* use client

* fix: rebase on 13.5

---------

Co-authored-by: Dmytro Hryshyn <dev.dmytroh@gmail.com>
2024-01-23 18:05:47 -03:00
MorganandOmar López 4417d649ea refactor: remove @calcom/features dependency to @calcom/web (#13365)
* refactor: remove @calcom/features dependency to @calcom/web

* refactor: remove IS_VISUAL_REGRESSION_TESTING from web

* fix: wrong imports

---------

Co-authored-by: Omar López <zomars@me.com>
2024-01-23 17:17:36 +00:00
Alex van Andel 7058da70e6 v3.7.6
release.yml / release (push) Canceled after 0s
2024-01-23 14:10:48 +00:00
Alex van Andel 2cb383c337 fix: Remove type from import getServerSideProps 2024-01-23 14:09:59 +00:00
Alex van Andel fe5eca8eb3 v3.7.5
release.yml / release (push) Canceled after 0s
2024-01-23 13:50:01 +00:00
Alex van Andel 7c6468953a fix: Update yarn.lock to include new apps 2024-01-23 13:48:15 +00:00
228 changed files with 1742 additions and 1376 deletions
+7 -1
View File
@@ -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"]
}
+98
View File
@@ -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": {},
}
`);
});
});
});
});
+1 -1
View File
@@ -38,7 +38,7 @@ async function handler(req: NextApiRequest, res: NextApiResponse) {
},
{}
)
: availableSlots;
: availableSlots.slots;
return { slots: slotsInProvidedTimeZone };
// eslint-disable-next-line @typescript-eslint/no-explicit-any
+9 -4
View File
@@ -1,11 +1,16 @@
"use client";
import { type DehydratedState, Hydrate } from "@tanstack/react-query";
import { type DehydratedState, HydrationBoundary } from "@tanstack/react-query";
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 }) {
const { state, children } = props;
@@ -16,6 +21,6 @@ export function createHydrateClient(opts: { transformer?: DataTransformer }) {
return state;
}, [state]);
return <Hydrate state={transformedState}>{children}</Hydrate>;
return <HydrationBoundary state={transformedState}>{children}</HydrationBoundary>;
};
}
+1 -4
View File
@@ -4,10 +4,7 @@ import { trpc } from "app/_trpc/client";
import { useState } from "react";
import superjson from "superjson";
import { httpBatchLink } from "@calcom/trpc/client/links/httpBatchLink";
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 { httpBatchLink, httpLink, loggerLink, splitLink } from "@calcom/trpc/client";
import { ENDPOINTS } from "@calcom/trpc/react/shared";
export type Endpoint = (typeof ENDPOINTS)[number];
+12
View File
@@ -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";
interface AdditionalCalendarSelectorProps {
isLoading?: boolean;
isPending?: boolean;
}
const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const AdditionalCalendarSelector = ({ isPending }: AdditionalCalendarSelectorProps): JSX.Element | null => {
const { t } = useLocale();
const query = trpc.viewer.integrations.useQuery({ variant: "calendar", onlyInstalled: true });
@@ -42,7 +42,7 @@ const AdditionalCalendarSelector = ({ isLoading }: AdditionalCalendarSelectorPro
return (
<Dropdown modal={false}>
<DropdownMenuTrigger asChild>
<Button StartIcon={Plus} color="secondary" {...(isLoading && { loading: isLoading })}>
<Button StartIcon={Plus} color="secondary" {...(isPending && { loading: isPending })}>
{t("add")}
</Button>
</DropdownMenuTrigger>
+18 -16
View File
@@ -1,6 +1,6 @@
import Link from "next/link";
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 { AppDependencyComponent, InstallAppButton } from "@calcom/app-store/components";
@@ -92,17 +92,19 @@ export const AppPage = ({
const [existingCredentials, setExistingCredentials] = useState<number[]>([]);
const [showDisconnectIntegration, setShowDisconnectIntegration] = useState(false);
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery(
{ appType: type },
{
onSettled(data) {
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
data?.userAdminTeams.length ? credentialsCount >= data?.userAdminTeams.length : credentialsCount > 0
);
setExistingCredentials(data?.credentials.map((credential) => credential.id) || []);
},
}
const appDbQuery = trpc.viewer.appCredentialsByType.useQuery({ appType: type });
useEffect(
function refactorMeWithoutEffect() {
const data = appDbQuery.data;
const credentialsCount = data?.credentials.length || 0;
setShowDisconnectIntegration(
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, {
@@ -190,7 +192,7 @@ export const AppPage = ({
)}
</header>
</div>
{!appDbQuery.isLoading ? (
{!appDbQuery.isPending ? (
isGlobal ||
(existingCredentials.length > 0 && allowedMultipleInstalls ? (
<div className="flex space-x-3">
@@ -211,7 +213,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -250,7 +252,7 @@ export const AppPage = ({
onClick: () => {
mutation.mutate({ type, variant, slug });
},
loading: mutation.isLoading,
loading: mutation.isPending,
};
}
return (
@@ -272,7 +274,7 @@ export const AppPage = ({
)}
{dependencies &&
(!dependencyData.isLoading ? (
(!dependencyData.isPending ? (
<div className="mt-6">
<AppDependencyComponent appName={name} dependencyData={dependencyData.data} />
</div>
@@ -28,7 +28,7 @@ type Props = {
onChanged: () => unknown | Promise<unknown>;
fromOnboarding?: boolean;
destinationCalendarId?: string;
isLoading?: boolean;
isPending?: boolean;
};
function CalendarList(props: Props) {
@@ -74,7 +74,7 @@ function ConnectedCalendarsList(props: Props) {
suspense: true,
refetchOnWindowFocus: false,
});
const { fromOnboarding, isLoading } = props;
const { fromOnboarding, isPending } = props;
return (
<QueryCell
query={query}
@@ -97,7 +97,7 @@ function ConnectedCalendarsList(props: Props) {
<div className="flex flex-col xl:flex-row xl:space-x-5">
{!!data.connectedCalendars.length && (
<div className="flex items-center">
<AdditionalCalendarSelector isLoading={isLoading} />
<AdditionalCalendarSelector isPending={isPending} />
</div>
)}
</div>
@@ -229,7 +229,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
hidePlaceholder
value={data.destinationCalendar?.externalId}
onChange={mutation.mutate}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</div>
</div>
@@ -239,7 +239,7 @@ export function CalendarListContainer(props: { heading?: boolean; fromOnboarding
onChanged={onChanged}
fromOnboarding={fromOnboarding}
destinationCalendarId={data.destinationCalendar?.externalId}
isLoading={mutation.isLoading}
isPending={mutation.isPending}
/>
</>
)}
@@ -100,9 +100,9 @@ export const InstallAppButtonChild = ({
<DropdownMenuContent
className="w-auto"
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">
<Spinner />
</div>
@@ -139,7 +139,7 @@ function BookingListItem(booking: BookingItemProps) {
setRejectionDialogIsOpen(true);
},
icon: Ban,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
// For bookings with payment, only confirm if the booking is paid for
...((isPending && !paymentAppData.enabled) ||
@@ -153,7 +153,7 @@ function BookingListItem(booking: BookingItemProps) {
bookingConfirm(true);
},
icon: Check,
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
]
: []),
@@ -293,7 +293,7 @@ function BookingListItem(booking: BookingItemProps) {
setViewRecordingsDialogIsOpen(true);
},
color: showCheckRecordingButton ? "secondary" : "primary",
disabled: mutation.isLoading,
disabled: mutation.isPending,
},
];
@@ -348,7 +348,7 @@ function BookingListItem(booking: BookingItemProps) {
<DialogFooter>
<DialogClose />
<Button
disabled={mutation.isLoading}
disabled={mutation.isPending}
data-testid="rejection-confirm"
onClick={() => {
bookingConfirm(false);
@@ -65,7 +65,7 @@ export const ChargeCardDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={chargeCardMutation.isLoading || chargeError}
disabled={chargeCardMutation.isPending || chargeError}
onClick={() =>
chargeCardMutation.mutate({
bookingId,
@@ -27,7 +27,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
const { isOpenDialog, setIsOpenDialog, bookingUId: bookingId } = props;
const [rescheduleReason, setRescheduleReason] = useState("");
const { mutate: rescheduleApi, isLoading } = trpc.viewer.bookings.requestReschedule.useMutation({
const { mutate: rescheduleApi, isPending } = trpc.viewer.bookings.requestReschedule.useMutation({
async onSuccess() {
showToast(t("reschedule_request_sent"), "success");
setIsOpenDialog(false);
@@ -65,7 +65,7 @@ export const RescheduleDialog = (props: IRescheduleDialog) => {
<DialogClose />
<Button
data-testid="send_request"
disabled={isLoading}
disabled={isPending}
onClick={() => {
rescheduleApi({
bookingId,
@@ -21,6 +21,7 @@ import type { EditableSchema } from "@calcom/features/form-builder/schema";
import { BookerLayoutSelector } from "@calcom/features/settings/BookerLayoutSelector";
import { classNames } from "@calcom/lib";
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 type { Prisma } from "@calcom/prisma/client";
import { trpc } from "@calcom/trpc/react";
@@ -35,7 +36,6 @@ import {
Tooltip,
} from "@calcom/ui";
import { Copy, Edit, Info } from "@calcom/ui/components/icon";
import { IS_VISUAL_REGRESSION_TESTING } from "@calcom/web/constants";
import RequiresConfirmationController from "./RequiresConfirmationController";
@@ -55,8 +55,8 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const { t } = useLocale();
const [showEventNameTip, setShowEventNameTip] = useState(false);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!eventType.hashedLink);
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!eventType.successRedirectUrl);
const [hashedLinkVisible, setHashedLinkVisible] = useState(!!formMethods.getValues("hashedLink"));
const [redirectUrlVisible, setRedirectUrlVisible] = useState(!!formMethods.getValues("successRedirectUrl"));
const [hashedUrl, setHashedUrl] = useState(eventType.hashedLink?.link);
const bookingFields: Prisma.JsonObject = {};
@@ -66,27 +66,31 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
user?.theme === "dark" ||
(!user?.theme && typeof document !== "undefined" && document.documentElement.classList.contains("dark"));
eventType.bookingFields.forEach(({ name }) => {
formMethods.getValues().bookingFields.forEach(({ name }) => {
bookingFields[name] = `${name} input`;
});
const eventNameObject: EventNameObjectType = {
attendeeName: t("scheduler"),
eventType: eventType.title,
eventName: eventType.eventName,
host: eventType.users[0]?.name || "Nameless",
eventType: formMethods.getValues("title"),
eventName: formMethods.getValues("eventName"),
host: formMethods.getValues("users")[0]?.name || "Nameless",
bookingFields: bookingFields,
t,
};
const [requiresConfirmation, setRequiresConfirmation] = useState(eventType.requiresConfirmation);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${eventType.slug}`;
const [requiresConfirmation, setRequiresConfirmation] = useState(
formMethods.getValues("requiresConfirmation")
);
const placeholderHashedLink = `${CAL_URL}/d/${hashedUrl}/${formMethods.getValues("slug")}`;
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(() => {
!hashedUrl && setHashedUrl(generateHashedLink(eventType.users[0]?.id ?? team?.id));
}, [eventType.users, hashedUrl, team?.id]);
!hashedUrl && setHashedUrl(generateHashedLink(formMethods.getValues("users")[0]?.id ?? team?.id));
}, [formMethods.getValues("users"), hashedUrl, team?.id]);
const toggleGuests = (enabled: boolean) => {
const bookingFields = formMethods.getValues("bookingFields");
@@ -108,7 +112,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
};
const { shouldLockDisableProps } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -122,7 +126,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
const closeEventNameTip = () => setShowEventNameTip(false);
const setEventName = (value: string) => formMethods.setValue("eventName", value);
return (
<div className="flex flex-col space-y-4">
{/**
@@ -145,12 +148,9 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
</Link>
</div>
<Controller
control={formMethods.control}
name="destinationCalendar"
defaultValue={eventType.destinationCalendar || undefined}
render={({ field: { onChange, value } }) => (
<DestinationCalendarSelector
destinationCalendar={eventType.destinationCalendar}
value={value ? value.externalId : undefined}
onChange={onChange}
hidePlaceholder
@@ -167,7 +167,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
type="text"
{...shouldLockDisableProps("eventName")}
placeholder={eventNamePlaceholder}
defaultValue={eventType.eventName || ""}
{...formMethods.register("eventName")}
addOnSuffix={
<Button
@@ -194,7 +193,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
{...shouldLockDisableProps("bookingFields")}
dataStore={{
options: {
locations: getLocationsOptionsForSelect(eventType?.locations ?? [], t),
locations: getLocationsOptionsForSelect(formMethods.getValues("locations") ?? [], t),
},
}}
/>
@@ -203,15 +202,13 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<RequiresConfirmationController
eventType={eventType}
seatsEnabled={seatsEnabled}
metadata={eventType.metadata}
metadata={formMethods.getValues("metadata")}
requiresConfirmation={requiresConfirmation}
onRequiresConfirmation={setRequiresConfirmation}
/>
<Controller
name="requiresBookerEmailVerification"
control={formMethods.control}
defaultValue={eventType.requiresBookerEmailVerification}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -228,8 +225,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="hideCalendarNotes"
control={formMethods.control}
defaultValue={eventType.hideCalendarNotes}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -246,7 +241,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="successRedirectUrl"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -274,7 +268,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
placeholder={t("external_redirect_url")}
required={redirectUrlVisible}
type="text"
defaultValue={eventType.successRedirectUrl || ""}
{...formMethods.register("successRedirectUrl")}
/>
<div
@@ -327,7 +320,10 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
hint={t("private_link_hint")}
defaultValue={placeholderHashedLink}
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
color="minimal"
size="sm"
@@ -336,7 +332,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
aria-label="copy link"
onClick={() => {
navigator.clipboard.writeText(placeholderHashedLink);
if (eventType.hashedLink) {
if (formMethods.getValues("hashedLink")) {
showToast(t("private_link_copied"), "success");
} else {
showToast(t("enabled_after_update_description"), "warning");
@@ -353,8 +349,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
<Controller
name="seatsPerTimeSlotEnabled"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
@@ -378,7 +372,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
formMethods.setValue("requiresConfirmation", false);
setRequiresConfirmation(false);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("seatsPerTimeSlot", 2);
formMethods.setValue("seatsPerTimeSlot", eventType.seatsPerTimeSlot ?? 2);
} else {
formMethods.setValue("seatsPerTimeSlot", null);
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">
<Controller
name="seatsPerTimeSlot"
control={formMethods.control}
defaultValue={eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<div>
<TextField
@@ -399,7 +391,7 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
label={t("number_of_seats")}
type="number"
disabled={seatsLocked.disabled}
defaultValue={value || 2}
defaultValue={value}
min={1}
containerClassName="max-w-80"
addOnSuffix={<>{t("seats")}</>}
@@ -408,21 +400,29 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
}}
/>
<div className="mt-4">
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => formMethods.setValue("seatsShowAttendees", e.target.checked)}
defaultChecked={!!eventType.seatsShowAttendees}
<Controller
name="seatsShowAttendees"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_attendees")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
<div className="mt-2">
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) =>
formMethods.setValue("seatsShowAvailabilityCount", e.target.checked)
}
defaultChecked={!!eventType.seatsShowAvailabilityCount}
<Controller
name="seatsShowAvailabilityCount"
render={({ field: { value, onChange } }) => (
<CheckboxField
description={t("show_available_seats_count")}
disabled={seatsLocked.disabled}
onChange={(e) => onChange(e)}
checked={value}
/>
)}
/>
</div>
</div>
@@ -436,8 +436,6 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
/>
<Controller
name="lockTimeZoneToggleOnBookingPage"
control={formMethods.control}
defaultValue={eventType.lockTimeZoneToggleOnBookingPage}
render={({ field: { value, onChange } }) => (
<SettingsToggle
labelClassName="text-sm"
@@ -452,59 +450,47 @@ export const EventAdvancedTab = ({ eventType, team }: Pick<EventTypeSetupProps,
)}
/>
{allowDisablingAttendeeConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
control={formMethods.control}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.attendee", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.attendee"
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_attendees_confirmation_emails")}
description={t("disable_attendees_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{allowDisablingHostConfirmationEmails(workflows) && (
<>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
control={formMethods.control}
defaultValue={!!eventType.seatsPerTimeSlot}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value || false}
onCheckedChange={(e) => {
formMethods.setValue("metadata.disableStandardEmails.confirmation.host", e);
onChange(e);
}}
/>
</>
)}
/>
</>
<Controller
name="metadata.disableStandardEmails.confirmation.host"
defaultValue={!!formMethods.getValues("seatsPerTimeSlot")}
render={({ field: { value, onChange } }) => (
<>
<SettingsToggle
labelClassName="text-sm"
toggleSwitchAtTheEnd={true}
switchContainerClassName="border-subtle rounded-lg border py-6 px-4 sm:px-6"
title={t("disable_host_confirmation_emails")}
description={t("disable_host_confirmation_emails_description")}
checked={value}
onCheckedChange={(e) => onChange(e)}
/>
</>
)}
/>
)}
{showEventNameTip && (
<CustomEventTypeModal
close={closeEventNameTip}
setValue={setEventName}
defaultValue={formMethods.getValues("eventName") || eventType.eventName || ""}
setValue={(val: string) => formMethods.setValue("eventName", val)}
defaultValue={formMethods.getValues("eventName")}
placeHolder={eventNamePlaceholder}
event={eventNameObject}
/>
+9 -10
View File
@@ -18,21 +18,21 @@ export type EventType = Pick<EventTypeSetupProps, "eventType">["eventType"] &
export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
const { t } = useLocale();
const { data: eventTypeApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: eventTypeApps, isPending } = trpc.viewer.integrations.useQuery({
extendsFeature: "EventType",
teamId: eventType.team?.id || eventType.parent?.teamId,
});
const methods = useFormContext<FormValues>();
const formMethods = useFormContext<FormValues>();
const installedApps =
eventTypeApps?.items.filter((app) => app.userCredentialIds.length || app.teams.length) || [];
const notInstalledApps =
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) => {
methods.setValue("metadata", {
...methods.getValues("metadata"),
formMethods.setValue("metadata", {
...formMethods.getValues("metadata"),
apps: _allAppsData,
});
};
@@ -47,7 +47,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
};
};
const eventTypeFormMetadata = methods.getValues("metadata");
const eventTypeFormMetadata = formMethods.getValues("metadata");
const getAppDataSetter = (
appId: EventTypeAppsList,
@@ -56,8 +56,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
): SetAppData => {
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)
const allAppsDataFromForm = methods.getValues("metadata")?.apps || {};
const allAppsDataFromForm = formMethods.getValues("metadata")?.apps || {};
const appData = allAppsDataFromForm[appId];
setAllAppsData({
...allAppsDataFromForm,
@@ -138,7 +137,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
message={t("locked_apps_description")}
/>
)}
{!isLoading && !installedApps?.length ? (
{!isPending && !installedApps?.length ? (
<EmptyScreen
Icon={Grid}
headline={t("empty_installed_apps_headline")}
@@ -179,7 +178,7 @@ export const EventAppsTab = ({ eventType }: { eventType: EventType }) => {
</div>
{!shouldLockDisableProps("apps").disabled && (
<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]">
{t("available_apps_lower_case")}
@@ -85,7 +85,7 @@ const EventTypeScheduleDetails = memo(
const { watch } = useFormContext<FormValues>();
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 || loggedInUser?.defaultScheduleId || selectedScheduleValue?.value || undefined,
@@ -112,7 +112,7 @@ const EventTypeScheduleDetails = memo(
)}>
{day}
</span>
{isLoading ? (
{isPending ? (
<SkeletonText className="block h-5 w-60" />
) : isAvailable ? (
<div className="space-y-3 text-right">
@@ -142,7 +142,7 @@ const EventTypeScheduleDetails = memo(
{!!schedule?.id && !schedule.isManaged && !schedule.readOnly && (
<Button
href={`/availability/${schedule.id}`}
disabled={isLoading}
disabled={isPending}
color="minimal"
EndIcon={ExternalLink}
target="_blank"
@@ -169,8 +169,15 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
const watchSchedule = watch("schedule");
const [options, setOptions] = useState<AvailabilityOption[]>([]);
const { isLoading } = trpc.viewer.availability.list.useQuery(undefined, {
onSuccess: ({ schedules }) => {
const { data, isPending } = trpc.viewer.availability.list.useQuery(undefined);
useEffect(
function refactorMeWithoutEffect() {
if (!data) {
return;
}
const schedules = data.schedules;
const options = schedules.map((schedule) => ({
value: schedule.id,
label: schedule.name,
@@ -225,8 +232,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
setValue("availability", value);
},
});
[data]
);
const availabilityValue = watch("availability");
useEffect(() => {
@@ -241,8 +248,8 @@ const EventTypeSchedule = ({ eventType }: { eventType: EventTypeSetup }) => {
{t("availability")}
{shouldLockIndicator("availability")}
</label>
{isLoading && <SelectSkeletonLoader />}
{!isLoading && (
{isPending && <SelectSkeletonLoader />}
{!isPending && (
<Controller
name="schedule"
render={({ field }) => {
@@ -1,10 +1,10 @@
import { useAutoAnimate } from "@formkit/auto-animate/react";
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 React, { useEffect, useState } from "react";
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 { 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 formMethods = useFormContext<FormValues>();
@@ -126,36 +126,20 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
},
];
const 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 watchPeriodType = formMethods.watch("periodType");
const optionsPeriod = [
{ value: 1, label: t("calendar_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 offsetStartValue = useWatch({
control: formMethods.control,
name: "offsetStart",
});
const [offsetToggle, setOffsetToggle] = useState(() => offsetStartValue > 0);
const [offsetToggle, setOffsetToggle] = useState(formMethods.getValues("offsetStart") > 0);
// Preview how the offset will affect start times
const watchOffsetStartValue = formMethods.watch("offsetStart");
const offsetOriginalTime = new Date();
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 (
<div>
@@ -165,8 +149,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="beforeBufferTime">{t("before_event")}</Label>
<Controller
name="beforeBufferTime"
control={formMethods.control}
defaultValue={eventType.beforeEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const beforeBufferOptions = [
{
@@ -197,8 +179,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="afterBufferTime">{t("after_event")}</Label>
<Controller
name="afterBufferTime"
control={formMethods.control}
defaultValue={eventType.afterEventBuffer || 0}
render={({ field: { onChange, value } }) => {
const afterBufferOptions = [
{
@@ -235,7 +215,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
<Label htmlFor="slotInterval">{t("slot_interval")}</Label>
<Controller
name="slotInterval"
control={formMethods.control}
render={() => {
const slotIntervalOptions = [
{
@@ -254,8 +233,9 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
formMethods.setValue("slotInterval", val && (val.value || 0) > 0 ? val.value : null);
}}
defaultValue={
slotIntervalOptions.find((option) => option.value === eventType.slotInterval) ||
slotIntervalOptions[0]
slotIntervalOptions.find(
(option) => option.value === formMethods.getValues("slotInterval")
) || slotIntervalOptions[0]
}
options={slotIntervalOptions}
/>
@@ -267,7 +247,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
</div>
<Controller
name="bookingLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -300,7 +279,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="onlyShowFirstAvailableSlot"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value;
return (
@@ -323,7 +301,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="durationLimits"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = Object.keys(value ?? {}).length > 0;
return (
@@ -361,7 +338,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
/>
<Controller
name="periodType"
control={formMethods.control}
render={({ field: { value } }) => {
const isChecked = value && value !== "UNLIMITED";
@@ -380,7 +356,6 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
onCheckedChange={(bool) => formMethods.setValue("periodType", bool ? "ROLLING" : "UNLIMITED")}>
<div className="border-subtle rounded-b-lg border border-t-0 p-6">
<RadioGroup.Root
defaultValue={watchPeriodType}
value={watchPeriodType}
onValueChange={(val) => formMethods.setValue("periodType", val as PeriodType)}>
{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"
placeholder="30"
{...formMethods.register("periodDays", { valueAsNumber: true })}
defaultValue={eventType.periodDays || 30}
/>
<Select
options={optionsPeriod}
isSearchable={false}
onChange={(opt) => {
formMethods.setValue(
"periodCountCalendarDays",
opt?.value.toString() as "0" | "1"
);
}}
defaultValue={
optionsPeriod.find(
(opt) => opt.value === (eventType.periodCountCalendarDays ? 1 : 0)
) ?? optionsPeriod[0]
onChange={(opt) =>
formMethods.setValue("periodCountCalendarDays", opt?.value === 1 ? "1" : "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>
)}
@@ -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">
<Controller
name="periodDates"
control={formMethods.control}
defaultValue={periodDates}
render={() => (
<DateRangePicker
startDate={formMethods.getValues("periodDates").startDate}
@@ -481,7 +455,7 @@ export const EventLimitsTab = ({ eventType }: Pick<EventTypeSetupProps, "eventTy
type="number"
containerClassName="max-w-80"
label={t("offset_start")}
{...formMethods.register("offsetStart")}
{...formMethods.register("offsetStart", { setValueAs: (value) => Number(value) })}
addOnSuffix={<>{t("minutes")}</>}
hint={t("offset_start_description", {
originalTime: offsetOriginalTime.toLocaleTimeString(i18n.language, { timeStyle: "short" }),
@@ -9,9 +9,5 @@ export const EventRecurringTab = ({ eventType }: Pick<EventTypeSetupProps, "even
const requirePayment = paymentAppData.price > 0;
return (
<div className="">
<RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />
</div>
);
return <RecurringEventController paymentEnabled={requirePayment} eventType={eventType} />;
};
+27 -42
View File
@@ -48,21 +48,19 @@ const getLocationFromType = (
}
};
const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const getLocationInfo = ({
eventType,
locationOptions,
}: Pick<EventTypeSetupProps, "eventType" | "locationOptions">) => {
const locationAvailable =
props.eventType.locations &&
props.eventType.locations.length > 0 &&
props.locationOptions.some((op) =>
op.options.find((opt) => opt.value === props.eventType.locations[0].type)
);
const locationDetails = props.eventType.locations &&
props.eventType.locations.length > 0 &&
eventType.locations &&
eventType.locations.length > 0 &&
locationOptions.some((op) => op.options.find((opt) => opt.value === eventType.locations[0].type));
const locationDetails = eventType.locations &&
eventType.locations.length > 0 &&
!locationAvailable && {
slug: props.eventType.locations[0].type
.replace("integrations:", "")
.replace(":", "-")
.replace("_video", ""),
name: props.eventType.locations[0].type
slug: eventType.locations[0].type.replace("integrations:", "").replace(":", "-").replace("_video", ""),
name: eventType.locations[0].type
.replace("integrations:", "")
.replace(":", " ")
.replace("_video", "")
@@ -72,16 +70,11 @@ const getLocationInfo = (props: Pick<EventTypeSetupProps, "eventType" | "locatio
};
return { locationAvailable, locationDetails };
};
interface DescriptionEditorProps {
description?: string | null;
editable?: boolean;
}
const DescriptionEditor = (props: DescriptionEditorProps) => {
const DescriptionEditor = ({ isEditable }: { isEditable: boolean }) => {
const formMethods = useFormContext<FormValues>();
const [mounted, setIsMounted] = useState(false);
const { t } = useLocale();
const { description } = props;
const [firstRender, setFirstRender] = useState(true);
useEffect(() => {
setIsMounted(true);
@@ -89,11 +82,11 @@ const DescriptionEditor = (props: DescriptionEditorProps) => {
return mounted ? (
<Editor
getText={() => md.render(formMethods.getValues("description") || description || "")}
getText={() => md.render(formMethods.getValues("description") || "")}
setText={(value: string) => formMethods.setValue("description", turndown(value))}
excludedToolbarItems={["blockType"]}
placeholder={t("quick_video_meeting")}
editable={props.editable}
editable={isEditable}
firstRender={firstRender}
setFirstRender={setFirstRender}
/>
@@ -113,7 +106,9 @@ export const EventSetupTab = (
const { t } = useLocale();
const formMethods = useFormContext<FormValues>();
const { eventType, team, destinationCalendar } = props;
const [multipleDuration, setMultipleDuration] = useState(eventType.metadata?.multipleDuration);
const [multipleDuration, setMultipleDuration] = useState(
formMethods.getValues("metadata")?.multipleDuration
);
const orgBranding = useOrgBranding();
const seatsEnabled = formMethods.watch("seatsPerTimeSlotEnabled");
@@ -143,12 +138,12 @@ export const EventSetupTab = (
}>
>(multipleDurationOptions.filter((mdOpt) => multipleDuration?.includes(mdOpt.value)));
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 } =
useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_description"),
t("locked_fields_member_description")
);
@@ -195,7 +190,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -219,7 +213,6 @@ export const EventSetupTab = (
return (
<Controller
name={`locations.${index}.${eventLocationType.defaultValueVariable}`}
control={formMethods.control}
defaultValue={defaultValue}
render={({ field: { onChange, value } }) => {
return (
@@ -339,7 +332,7 @@ export const EventSetupTab = (
defaultChecked={defaultLocation?.displayLocationPublicly}
description={t("display_location_label")}
onChange={(e) => {
const fieldValues = formMethods.getValues().locations[index];
const fieldValues = formMethods.getValues("locations")[index];
updateLocationField(index, {
...fieldValues,
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.
Change it{" "}
<Link
href={`${CAL_URL}/event-types/${eventType.id}?tabName=advanced`}
href={`${CAL_URL}/event-types/${formMethods.getValues("id")}?tabName=advanced`}
className="underline">
here.
</Link>{" "}
@@ -463,7 +456,6 @@ export const EventSetupTab = (
required
label={t("title")}
{...shouldLockDisableProps("title")}
defaultValue={eventType.title}
{...formMethods.register("title")}
/>
<div>
@@ -471,23 +463,19 @@ export const EventSetupTab = (
{t("description")}
{shouldLockIndicator("description")}
</Label>
<DescriptionEditor
description={eventType?.description}
editable={!descriptionLockedProps.disabled}
/>
<DescriptionEditor isEditable={!descriptionLockedProps.disabled} />
</div>
<TextField
required
label={t("URL")}
{...shouldLockDisableProps("slug")}
defaultValue={eventType.slug}
addOnLeading={
<>
{urlPrefix}/
{!isManagedEventType
? team
? (orgBranding ? "" : "team/") + team.slug
: eventType.users[0].username
: formMethods.getValues("users")[0].username
: t("username_placeholder")}
/
</>
@@ -564,7 +552,7 @@ export const EventSetupTab = (
type="number"
{...lengthLockedProps}
label={t("duration")}
defaultValue={eventType.length ?? 15}
defaultValue={formMethods.getValues("length") ?? 15}
{...formMethods.register("length")}
addOnSuffix={<>{t("minutes")}</>}
min={1}
@@ -580,6 +568,8 @@ export const EventSetupTab = (
onCheckedChange={() => {
if (multipleDuration !== undefined) {
setMultipleDuration(undefined);
setSelectedMultipleDuration([]);
setDefaultDuration(null);
formMethods.setValue("metadata.multipleDuration", undefined);
formMethods.setValue("length", eventType.length);
} else {
@@ -600,12 +590,7 @@ export const EventSetupTab = (
{shouldLockIndicator("locations")}
</Skeleton>
<Controller
name="locations"
control={formMethods.control}
defaultValue={eventType.locations || []}
render={() => <Locations />}
/>
<Controller name="locations" render={() => <Locations />} />
</div>
</div>
</div>
@@ -67,55 +67,66 @@ type Props = {
availability?: AvailabilityOption;
isUserOrganizationAdmin: boolean;
bookerUrl: string;
activeWebhooksNumber: number;
};
function getNavigation(props: {
type getNavigationProps = {
t: TFunction;
eventType: Props["eventType"];
length: number;
id: number;
multipleDuration?: EventTypeSetupProps["eventType"]["metadata"]["multipleDuration"];
enabledAppsNumber: number;
enabledWorkflowsNumber: number;
installedAppsNumber: number;
availability: AvailabilityOption | undefined;
}) {
const { eventType, t, enabledAppsNumber, installedAppsNumber, enabledWorkflowsNumber } = props;
const duration =
eventType.metadata?.multipleDuration?.map((duration) => ` ${duration}`) || eventType.length;
};
function getNavigation({
length,
id,
multipleDuration,
t,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
}: getNavigationProps) {
const duration = multipleDuration?.map((duration) => ` ${duration}`) || length;
return [
{
name: "event_setup_tab_title",
href: `/event-types/${eventType.id}?tabName=setup`,
href: `/event-types/${id}?tabName=setup`,
icon: LinkIcon,
info: `${duration} ${t("minute_timeUnit")}`, // TODO: Get this from props
},
{
name: "event_limit_tab_title",
href: `/event-types/${eventType.id}?tabName=limits`,
href: `/event-types/${id}?tabName=limits`,
icon: Clock,
info: `event_limit_tab_description`,
},
{
name: "event_advanced_tab_title",
href: `/event-types/${eventType.id}?tabName=advanced`,
href: `/event-types/${id}?tabName=advanced`,
icon: Sliders,
info: `event_advanced_tab_description`,
},
{
name: "recurring",
href: `/event-types/${eventType.id}?tabName=recurring`,
href: `/event-types/${id}?tabName=recurring`,
icon: Repeat,
info: `recurring_event_tab_description`,
},
{
name: "apps",
href: `/event-types/${eventType.id}?tabName=apps`,
href: `/event-types/${id}?tabName=apps`,
icon: Grid,
//TODO: Handle proper translation with count handling
info: `${installedAppsNumber} apps, ${enabledAppsNumber} ${t("active")}`,
},
{
name: "workflows",
href: `/event-types/${eventType.id}?tabName=workflows`,
href: `/event-types/${id}?tabName=workflows`,
icon: Zap,
info: `${enabledWorkflowsNumber} ${t("active")}`,
},
@@ -136,6 +147,7 @@ function EventTypeSingleLayout({
availability,
isUserOrganizationAdmin,
bookerUrl,
activeWebhooksNumber,
}: Props) {
const utils = trpc.useContext();
const { t } = useLocale();
@@ -145,7 +157,7 @@ function EventTypeSingleLayout({
const hasPermsToDelete =
currentUserMembership?.role !== "MEMBER" ||
!currentUserMembership ||
eventType.schedulingType === SchedulingType.MANAGED ||
formMethods.getValues("schedulingType") === SchedulingType.MANAGED ||
isUserOrganizationAdmin;
const deleteMutation = trpc.viewer.eventTypes.delete.useMutation({
@@ -167,16 +179,23 @@ function EventTypeSingleLayout({
});
const { isManagedEventType, isChildrenManagedEventType } = useLockedFieldsManager(
eventType,
formMethods.getValues(),
t("locked_fields_admin_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
const EventTypeTabs = useMemo(() => {
const navigation = getNavigation({
t,
eventType,
length,
multipleDuration,
id: formMethods.getValues("id"),
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -185,31 +204,29 @@ function EventTypeSingleLayout({
navigation.splice(1, 0, {
name: "availability",
href: `/event-types/${eventType.id}?tabName=availability`,
href: `/event-types/${formMethods.getValues("id")}?tabName=availability`,
icon: Calendar,
info:
isManagedEventType || isChildrenManagedEventType
? eventType.schedule === null
? formMethods.getValues("schedule") === null
? "members_default_schedule"
: isChildrenManagedEventType
? `${
eventType.scheduleName
? `${eventType.scheduleName} - ${t("managed")}`
formMethods.getValues("scheduleName")
? `${formMethods.getValues("scheduleName")} - ${t("managed")}`
: `default_schedule_name`
}`
: eventType.scheduleName ?? `default_schedule_name`
: eventType.scheduleName ?? `default_schedule_name`,
: formMethods.getValues("scheduleName") ?? `default_schedule_name`
: formMethods.getValues("scheduleName") ?? `default_schedule_name`,
});
// If there is a team put this navigation item within the tabs
if (team) {
navigation.splice(2, 0, {
name: "assignment",
href: `/event-types/${eventType.id}?tabName=team`,
href: `/event-types/${formMethods.getValues("id")}?tabName=team`,
icon: Users,
info: `${t(eventType.schedulingType?.toLowerCase() ?? "")}${
isManagedEventType
? ` - ${t("number_member", { count: formMethods.watch("children").length || 0 })}`
: ""
info: `${t(watchSchedulingType?.toLowerCase() ?? "")}${
isManagedEventType ? ` - ${t("number_member", { count: watchChildrenCount || 0 })}` : ""
}`,
});
}
@@ -225,15 +242,14 @@ function EventTypeSingleLayout({
}
navigation.push({
name: "webhooks",
href: `/event-types/${eventType.id}?tabName=webhooks`,
href: `/event-types/${formMethods.getValues("id")}?tabName=webhooks`,
icon: TbWebhook,
info: `${eventType.webhooks.filter((webhook) => webhook.active).length} ${t("active")}`,
info: `${activeWebhooksNumber} ${t("active")}`,
});
}
return navigation;
}, [
t,
eventType,
enabledAppsNumber,
installedAppsNumber,
enabledWorkflowsNumber,
@@ -241,16 +257,23 @@ function EventTypeSingleLayout({
isManagedEventType,
isChildrenManagedEventType,
team,
formMethods,
length,
multipleDuration,
formMethods.getValues("id"),
watchSchedulingType,
watchChildrenCount,
activeWebhooksNumber,
]);
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}`;
const embedLink = `${team ? `team/${team.slug}` : eventType.users[0].username}/${eventType.slug}`;
const isManagedEvent = eventType.schedulingType === SchedulingType.MANAGED ? "_managed" : "";
const embedLink = `${
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 (
<Shell
backPath="/event-types"
@@ -258,7 +281,7 @@ function EventTypeSingleLayout({
heading={eventType.title}
CTA={
<div className="flex items-center justify-end">
{!eventType.metadata?.managedEventConfig && (
{!formMethods.getValues("metadata")?.managedEventConfig && (
<>
<div
className={classNames(
@@ -333,7 +356,7 @@ function EventTypeSingleLayout({
tooltip={t("embed")}
tooltipSide="bottom"
tooltipOffset={4}
eventId={eventType.id}
eventId={formMethods.getValues("id")}
/>
</>
)}
@@ -446,14 +469,14 @@ function EventTypeSingleLayout({
</Suspense>
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
variety="danger"
title={t(`delete${isManagedEvent}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
onConfirm={(e) => {
e.preventDefault();
deleteMutation.mutate({ id: eventType.id });
deleteMutation.mutate({ id: formMethods.getValues("id") });
}}>
<p className="mt-5">
<Trans
@@ -35,8 +35,9 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const editWebhookMutation = trpc.viewer.webhook.edit.useMutation({
async onSuccess() {
setEditModalOpen(false);
await utils.viewer.webhook.list.invalidate();
showToast(t("webhook_updated_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -45,9 +46,10 @@ export const EventWebhooksTab = ({ eventType }: Pick<EventTypeSetupProps, "event
const createWebhookMutation = trpc.viewer.webhook.create.useMutation({
async onSuccess() {
setCreateModalOpen(false);
showToast(t("webhook_created_successfully"), "success");
await utils.viewer.webhook.list.invalidate();
setCreateModalOpen(false);
await utils.viewer.eventTypes.get.invalidate();
},
onError(error) {
showToast(`${error.message}`, "error");
@@ -108,7 +108,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
eventTypeId: eventType.id,
eventTriggers: [WebhookTriggerEvents.INSTANT_MEETING],
});
const { data: installedApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: installedApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: true,
});
@@ -188,7 +188,7 @@ const InstantMeetingWebhooks = ({ eventType }: { eventType: EventTypeSetup }) =>
return (
<div>
{webhooks && !isLoading && (
{webhooks && !isPending && (
<>
<div>
{webhooks.length ? (
@@ -8,18 +8,18 @@ interface ManagedEventDialogProps {
slugExistsChildrenDialogOpen: ChildrenEventType[];
slug: string;
onOpenChange: () => void;
isLoading: boolean;
isPending: boolean;
onConfirm: (e: { preventDefault: () => void }) => void;
}
export default function ManagedEventDialog(props: ManagedEventDialogProps) {
const { t } = useLocale();
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isLoading, onConfirm } = props;
const { slugExistsChildrenDialogOpen, slug, onOpenChange, isPending, onConfirm } = props;
return (
<Dialog open={slugExistsChildrenDialogOpen.length > 0} onOpenChange={onOpenChange}>
<ConfirmationDialogContent
isLoading={isLoading}
isPending={isPending}
variety="warning"
title={t("managed_event_dialog_title", {
slug,
@@ -19,11 +19,10 @@ export default function RecurringEventController({
paymentEnabled,
}: RecurringEventControllerProps) {
const { t } = useLocale();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
eventType.recurringEvent
);
const formMethods = useFormContext<FormValues>();
const [recurringEventState, setRecurringEventState] = useState<RecurringEvent | null>(
formMethods.getValues("recurringEvent")
);
/* Just yearly-0, monthly-1 and weekly-2 */
const recurringEventFreqOptions = Object.entries(Frequency)
.filter(([key, value]) => isNaN(Number(key)) && Number(value) < 3)
@@ -28,7 +28,7 @@ const AppConnectionItem = (props: IAppConnectionItem) => {
color="secondary"
disabled={installed}
type="button"
loading={buttonProps?.isLoading}
loading={buttonProps?.isPending}
onClick={(event) => {
// Save cookie key to return url step
document.cookie = `return-to=${window.location.href};path=/;max-age=3600;SameSite=Lax`;
@@ -31,7 +31,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
return (
<>
{/* Already connected calendars */}
{!queryConnectedCalendars.isLoading &&
{!queryConnectedCalendars.isPending &&
firstCalendar &&
firstCalendar.integration &&
firstCalendar.integration.title &&
@@ -76,7 +76,7 @@ const ConnectedCalendars = (props: IConnectCalendarsProps) => {
</List>
)}
{queryIntegrations.isLoading && <StepConnectionLoader />}
{queryIntegrations.isPending && <StepConnectionLoader />}
<button
type="button"
@@ -13,7 +13,7 @@ interface ConnectedAppStepProps {
const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
const { nextStep } = props;
const { data: queryConnectedVideoApps, isLoading } = trpc.viewer.integrations.useQuery({
const { data: queryConnectedVideoApps, isPending } = trpc.viewer.integrations.useQuery({
variant: "conferencing",
onlyInstalled: false,
sortByMostPopular: true,
@@ -26,7 +26,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
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">
{queryConnectedVideoApps?.items &&
queryConnectedVideoApps?.items.map((item) => {
@@ -48,7 +48,7 @@ const ConnectedVideoStep = (props: ConnectedAppStepProps) => {
</List>
)}
{isLoading && <StepConnectionLoader />}
{isPending && <StepConnectionLoader />}
<button
type="button"
data-testid="save-video-button"
@@ -113,7 +113,7 @@ const UserSettings = (props: IUserSettingsProps) => {
<Button
type="submit"
className="mt-8 flex w-full flex-row justify-center"
disabled={mutation.isLoading}>
disabled={mutation.isPending}>
{t("next_step_text")}
<ArrowRight className="ml-2 h-4 w-4 self-center" aria-hidden="true" />
</Button>
+2 -2
View File
@@ -7,7 +7,7 @@ import { Check } from "@calcom/ui/components/icon";
const StepDone = (props: {
currentStep: number;
nextStepPath: string;
setIsLoading: Dispatch<SetStateAction<boolean>>;
setIsPending: Dispatch<SetStateAction<boolean>>;
}) => {
const router = useRouter();
const { t } = useLocale();
@@ -18,7 +18,7 @@ const StepDone = (props: {
name={`wizard-step-${props.currentStep}`}
className="flex justify-center space-y-4"
onSubmit={(e) => {
props.setIsLoading(true);
props.setIsPending(true);
e.preventDefault();
router.replace(props.nextStepPath);
}}>
@@ -293,7 +293,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{usernameChangeCondition === UsernameChangeStatusEnum.UPGRADE && (
<Button
type="button"
loading={updateUsername.isLoading}
loading={updateUsername.isPending}
data-testid="go-to-billing"
href={paymentLink}>
<>
@@ -305,7 +305,7 @@ const PremiumTextfield = (props: ICustomUsernameProps) => {
{usernameChangeCondition !== UsernameChangeStatusEnum.UPGRADE && (
<Button
type="button"
loading={updateUsername.isLoading}
loading={updateUsername.isPending}
data-testid="save-username"
onClick={() => {
saveUsername();
@@ -177,7 +177,7 @@ const UsernameTextfield = (props: ICustomUsernameProps & Partial<React.Component
<DialogFooter className="mt-4">
<Button
type="button"
loading={updateUsernameMutation.isLoading}
loading={updateUsernameMutation.isPending}
data-testid="save-username"
onClick={updateUsername}>
{t("save")}
@@ -2,8 +2,7 @@ import type { InputHTMLAttributes } from "react";
import React, { forwardRef } from "react";
import classNames from "@calcom/lib/classNames";
import InfoBadge from "@components/ui/InfoBadge";
import { InfoBadge } from "@calcom/ui";
type Props = InputHTMLAttributes<HTMLInputElement> & {
label?: React.ReactNode;
-1
View File
@@ -1 +0,0 @@
export const IS_VISUAL_REGRESSION_TESTING = Boolean(globalThis.window?.Meticulous?.isRunningAsTest);
+8 -36
View File
@@ -1,21 +1,15 @@
import type {
QueryObserverLoadingErrorResult,
QueryObserverLoadingResult,
QueryObserverPendingResult,
QueryObserverRefetchErrorResult,
QueryObserverSuccessResult,
QueryObserverLoadingErrorResult,
UseQueryResult,
} from "@tanstack/react-query";
import type { ReactNode } from "react";
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 type { UseTRPCQueryOptions } from "@trpc/react-query/shared";
type ErrorLike = {
message: string;
};
@@ -27,7 +21,7 @@ interface QueryCellOptionsBase<TData, TError extends ErrorLike> {
error?: (
query: QueryObserverLoadingErrorResult<TData, TError> | QueryObserverRefetchErrorResult<TData, TError>
) => JSXElementOrNull;
loading?: (query: QueryObserverLoadingResult<TData, TError> | null) => JSXElementOrNull;
loading?: (query: QueryObserverPendingResult<TData, TError> | null) => JSXElementOrNull;
}
interface QueryCellOptionsNoEmpty<TData, TError extends ErrorLike>
@@ -58,8 +52,11 @@ export function QueryCell<TData, TError extends ErrorLike>(
const { isLocaleReady } = useLocale();
const StatusLoader = opts.customLoader || <Loader />; // Fixes edge case where this can return null form query cell
if (query.status === "loading" || !isLocaleReady) {
return opts.loading?.(query.status === "loading" ? query : null) ?? StatusLoader;
if (!isLocaleReady) {
return opts.loading?.(query.status === "pending" ? query : null) ?? StatusLoader;
}
if (query.status === "pending") {
return opts.loading?.(query) ?? StatusLoader;
}
if (query.status === "success") {
@@ -81,28 +78,3 @@ export function QueryCell<TData, TError extends ErrorLike>(
// impossible state
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 };
+2 -2
View File
@@ -1,6 +1,6 @@
{
"name": "@calcom/web",
"version": "3.7.4",
"version": "3.7.7",
"private": true,
"scripts": {
"analyze": "ANALYZE=true next build",
@@ -60,7 +60,7 @@
"@sentry/nextjs": "^7.73.0",
"@stripe/react-stripe-js": "^1.10.0",
"@stripe/stripe-js": "^1.35.0",
"@tanstack/react-query": "^4.3.9",
"@tanstack/react-query": "^5.17.15",
"@tremor/react": "^2.0.0",
"@types/turndown": "^5.0.1",
"@upstash/ratelimit": "^0.4.3",
@@ -1,9 +1,9 @@
import jwt from "jsonwebtoken";
import type { NextApiRequest, NextApiResponse } from "next";
import type { OAuthTokenPayload } from "pages/api/auth/oauth/token";
import prisma from "@calcom/prisma";
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) {
if (req.method !== "POST") {
+1 -8
View File
@@ -3,14 +3,7 @@ import type { NextApiRequest, NextApiResponse } from "next";
import prisma from "@calcom/prisma";
import { generateSecret } from "@calcom/trpc/server/routers/viewer/oAuth/addClient.handler";
export type OAuthTokenPayload = {
userId?: number | null;
teamId?: number | null;
token_type: string;
scope: string[];
clientId: string;
};
import type { OAuthTokenPayload } from "@calcom/types/oauth";
export default async function handler(req: NextApiRequest, res: NextApiResponse) {
if (req.method !== "POST") {
+11 -6
View File
@@ -5,7 +5,7 @@ import classNames from "classnames";
import { signIn } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useState, useEffect } from "react";
import { FormProvider, useForm } from "react-hook-form";
import { FaGoogle } from "react-icons/fa";
import { z } from "zod";
@@ -157,15 +157,20 @@ inferSSRProps<typeof getServerSideProps> & WithNonceProps<{}>) {
else setErrorMessage(errorMessages[res.error] || t("something_went_wrong"));
};
const { data, isLoading } = trpc.viewer.public.ssoConnections.useQuery(undefined, {
onError: (err) => {
setErrorMessage(err.message);
const { data, isPending, error } = trpc.viewer.public.ssoConnections.useQuery();
useEffect(
function refactorMeWithoutEffect() {
if (error) {
setErrorMessage(error.message);
}
},
});
[error]
);
const displaySSOLogin = HOSTED_CAL_FEATURES
? true
: isSAMLLoginEnabled && !isLoading && data?.connectionExists;
: isSAMLLoginEnabled && !isPending && data?.connectionExists;
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]">
+5 -5
View File
@@ -28,7 +28,7 @@ export default function Authorize() {
const [selectedAccount, setSelectedAccount] = useState<{ value: string; label: string } | null>();
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,
},
@@ -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({
onSuccess: (data) => {
@@ -58,7 +58,7 @@ export default function Authorize() {
if (mappedProfiles.length > 0) {
setSelectedAccount(mappedProfiles[0]);
}
}, [isLoadingProfiles]);
}, [isPendingProfiles]);
useEffect(() => {
if (status === "unauthenticated") {
@@ -69,9 +69,9 @@ export default function Authorize() {
}
}, [status]);
const isLoading = isLoadingGetClient || isLoadingProfiles || status !== "authenticated";
const isPending = isPendingGetClient || isPendingProfiles || status !== "authenticated";
if (isLoading) {
if (isPending) {
return <></>;
}
+13 -62
View File
@@ -1,15 +1,12 @@
import type { GetServerSidePropsContext } from "next";
"use client";
import { usePathname, useRouter } from "next/navigation";
import { useState } from "react";
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 { useCompatSearchParams } from "@calcom/lib/hooks/useCompatSearchParams";
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 { Meta, WizardForm } from "@calcom/ui";
@@ -18,7 +15,7 @@ import { AdminUserContainer as AdminUser } from "@components/setup/AdminUser";
import ChooseLicense from "@components/setup/ChooseLicense";
import EnterpriseLicense from "@components/setup/EnterpriseLicense";
import { ssrInit } from "@server/lib/ssr";
import { getServerSideProps } from "@server/lib/setup/getServerSideProps";
function useSetStep() {
const router = useRouter();
@@ -44,16 +41,16 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{
title: t("administrator_user"),
description: t("lets_create_first_administrator_user"),
content: (setIsLoading) => (
content: (setIsPending) => (
<AdminUser
onSubmit={() => {
setIsLoading(true);
setIsPending(true);
}}
onSuccess={() => {
setStep(2);
}}
onError={() => {
setIsLoading(false);
setIsPending(false);
}}
userCount={props.userCount}
/>
@@ -62,7 +59,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
{
title: t("choose_a_license"),
description: t("choose_license_description"),
content: (setIsLoading) => {
content: (setIsPending) => {
return (
<ChooseLicense
id="wizard-step-2"
@@ -70,7 +67,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
value={value}
onChange={setValue}
onSubmit={() => {
setIsLoading(true);
setIsPending(true);
setStep(3);
}}
/>
@@ -83,14 +80,14 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
steps.push({
title: t("step_enterprise_license"),
description: t("step_enterprise_license_description"),
content: (setIsLoading) => {
content: (setIsPending) => {
const currentStep = 3;
return (
<EnterpriseLicense
id={`wizard-step-${currentStep}`}
name={`wizard-step-${currentStep}`}
onSubmit={() => {
setIsLoading(true);
setIsPending(true);
}}
onSuccess={() => {
setStep(currentStep + 1);
@@ -109,7 +106,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
title: t("enable_apps"),
description: t("enable_apps_description", { appName: APP_NAME }),
contentClassname: "!pb-0 mb-[-1px]",
content: (setIsLoading) => {
content: (setIsPending) => {
const currentStep = isFreeLicense ? 3 : 4;
return (
<AdminAppsList
@@ -123,7 +120,7 @@ export function Setup(props: inferSSRProps<typeof getServerSideProps>) {
baseURL={`/auth/setup?step=${currentStep}`}
useQueryParam={true}
onSubmit={() => {
setIsLoading(true);
setIsPending(true);
router.replace("/");
}}
/>
@@ -152,50 +149,4 @@ Setup.isThemeSupported = false;
Setup.PageWrapper = PageWrapper;
export default Setup;
export const getServerSideProps = async (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,
},
};
};
export { getServerSideProps };
+1 -1
View File
@@ -44,7 +44,7 @@ function VerifyEmailPage() {
<Button
color="minimal"
className="underline"
loading={mutation.isLoading}
loading={mutation.isPending}
onClick={() => {
showToast("Send email", "success");
mutation.mutate();
+7 -7
View File
@@ -93,7 +93,7 @@ export default function Availability() {
const fromEventType = searchParams?.get("fromEventType");
const { timeFormat } = me.data || { timeFormat: null };
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 },
{
enabled: !!scheduleId,
@@ -158,7 +158,7 @@ export default function Availability() {
control={form.control}
name="name"
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>
<Switch
id="hiddenSwitch"
disabled={isLoading || schedule?.isDefault}
disabled={isPending || schedule?.isDefault}
checked={form.watch("isDefault")}
onCheckedChange={(e) => {
form.setValue("isDefault", e);
@@ -210,7 +210,7 @@ export default function Availability() {
/>
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
@@ -249,7 +249,7 @@ export default function Availability() {
/>
</DialogTrigger>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
variety="danger"
title={t("delete_schedule")}
confirmBtnText={t("delete")}
@@ -284,7 +284,7 @@ export default function Availability() {
</Skeleton>
<Switch
id="hiddenSwitch"
disabled={isLoading || schedule?.isDefault}
disabled={isPending || schedule?.isDefault}
checked={form.watch("isDefault")}
onCheckedChange={(e) => {
form.setValue("isDefault", e);
@@ -334,7 +334,7 @@ export default function Availability() {
className="ml-4 lg:ml-0"
type="submit"
form="availability-form"
loading={updateMutation.isLoading}>
loading={updateMutation.isPending}>
{t("save")}
</Button>
<Button
+13 -11
View File
@@ -15,7 +15,7 @@ import { trpc } from "@calcom/trpc/react";
import { EmptyScreen, showToast, ToggleGroup } from "@calcom/ui";
import { Clock } from "@calcom/ui/components/icon";
import { withQuery } from "@lib/QueryCell";
import { QueryCell } from "@lib/QueryCell";
import PageWrapper from "@components/PageWrapper";
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
const WithQuery = withQuery(trpc.viewer.availability.list as any);
function AvailabilityListWithQuery() {
const query = trpc.viewer.availability.list.useQuery();
return (
<QueryCell
query={query}
success={({ data }) => <AvailabilityList {...data} />}
customLoader={<SkeletonLoader />}
/>
);
}
export default function AvailabilityPage() {
const { t } = useLocale();
@@ -179,14 +188,7 @@ export default function AvailabilityPage() {
<NewScheduleButton />
</div>
}>
{searchParams?.get("type") === "team" ? (
<AvailabilitySliderTable />
) : (
<WithQuery
success={({ data }) => <AvailabilityList {...data} />}
customLoader={<SkeletonLoader />}
/>
)}
{searchParams?.get("type") === "team" ? <AvailabilitySliderTable /> : <AvailabilityListWithQuery />}
</ShellMain>
</div>
);
+1 -1
View File
@@ -164,7 +164,7 @@ export default function Bookings() {
{query.status === "error" && (
<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 && (
<>
{!!bookingsToday.length && status === "upcoming" && (
+1 -1
View File
@@ -68,7 +68,7 @@ function ConnectAndJoin() {
</div>
) : (
<Button
loading={mutation.isLoading}
loading={mutation.isPending}
tooltip={isUserPartOfOrg ? t("join_meeting") : t("not_part_of_org")}
disabled={!isUserPartOfOrg}
onClick={() => {
+25 -6
View File
@@ -80,6 +80,7 @@ const EventWebhooksTab = dynamic(() =>
const ManagedEventTypeDialog = dynamic(() => import("@components/eventtype/ManagedEventDialog"));
export type FormValues = {
id: number;
title: string;
eventTitle: string;
eventName: string;
@@ -119,6 +120,7 @@ export type FormValues = {
seatsShowAttendees: boolean | null;
seatsShowAvailabilityCount: boolean | null;
seatsPerTimeSlotEnabled: boolean;
scheduleName: string;
minimumBookingNotice: number;
minimumBookingNoticeInDurationType: number;
beforeBufferTime: number;
@@ -139,6 +141,7 @@ export type FormValues = {
availability?: AvailabilityOption;
bookerLayouts: BookerLayoutSettings;
multipleDurationEnabled: boolean;
users: EventTypeSetup["users"];
};
export type CustomInputParsed = typeof customInputSchema._output;
@@ -180,7 +183,6 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const { eventType, locationOptions, team, teamMembers, currentUserMembership, destinationCalendar } = props;
const [animationParentRef] = useAutoAnimate<HTMLDivElement>();
const updateMutation = trpc.viewer.eventTypes.update.useMutation({
onSuccess: async () => {
formMethods.setValue(
@@ -248,6 +250,18 @@ const EventTypePage = (props: EventTypeSetupProps) => {
const defaultValues: any = useMemo(() => {
return {
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 || [],
recurringEvent: eventType.recurringEvent || null,
isInstantEvent: eventType.isInstantEvent,
@@ -268,9 +282,13 @@ const EventTypePage = (props: EventTypeSetupProps) => {
periodType: eventType.periodType,
periodCountCalendarDays: eventType.periodCountCalendarDays ? "1" : "0",
schedulingType: eventType.schedulingType,
requiresConfirmation: eventType.requiresConfirmation,
slotInterval: eventType.slotInterval,
minimumBookingNotice: eventType.minimumBookingNotice,
metadata,
hosts: eventType.hosts,
successRedirectUrl: eventType.successRedirectUrl || "",
users: eventType.users,
children: eventType.children.map((ch) => ({
...ch,
created: true,
@@ -411,7 +429,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
),
availability: <EventAvailabilityTab eventType={eventType} isTeamEvent={!!team} />,
team: <EventTeamTab teamMembers={teamMembers} team={team} eventType={eventType} />,
limits: <EventLimitsTab eventType={eventType} />,
limits: <EventLimitsTab />,
advanced: <EventAdvancedTab eventType={eventType} team={team} />,
instant: <EventInstantTab eventType={eventType} isTeamEvent={!!team} />,
recurring: <EventRecurringTab eventType={eventType} />,
@@ -490,7 +508,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
...rest,
length,
@@ -550,9 +568,10 @@ const EventTypePage = (props: EventTypeSetupProps) => {
installedAppsNumber={eventTypeApps?.items.length || 0}
enabledWorkflowsNumber={eventType.workflows.length}
eventType={eventType}
activeWebhooksNumber={eventType.webhooks.filter((webhook) => webhook.active).length}
team={team}
availability={availability}
isUpdateMutationLoading={updateMutation.isLoading}
isUpdateMutationLoading={updateMutation.isPending}
formMethods={formMethods}
// disableBorder={tabName === "apps" || tabName === "workflows" || tabName === "webhooks"}
disableBorder={true}
@@ -618,7 +637,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
throw new Error(t("event_setup_multiple_payment_apps_error"));
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { availability, ...rest } = input;
const { availability, users, scheduleName, ...rest } = input;
updateMutation.mutate({
...rest,
length,
@@ -647,7 +666,7 @@ const EventTypePage = (props: EventTypeSetupProps) => {
{slugExistsChildrenDialogOpen.length ? (
<ManagedEventTypeDialog
slugExistsChildrenDialogOpen={slugExistsChildrenDialogOpen}
isLoading={formMethods.formState.isSubmitting}
isPending={formMethods.formState.isSubmitting}
onOpenChange={() => {
setSlugExistsChildrenDialogOpen([]);
}}
+4 -4
View File
@@ -54,6 +54,7 @@ import {
Switch,
Tooltip,
ArrowButton,
UserAvatarGroup,
} from "@calcom/ui";
import {
Clipboard,
@@ -74,7 +75,6 @@ import useMeQuery from "@lib/hooks/useMeQuery";
import PageWrapper from "@components/PageWrapper";
import SkeletonLoader from "@components/eventtype/SkeletonLoader";
import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup";
type EventTypeGroups = RouterOutputs["viewer"]["eventTypes"]["getByViewer"]["eventTypeGroups"];
type EventTypeGroupProfile = EventTypeGroups[number]["profile"];
@@ -686,7 +686,7 @@ export const EventTypeList = ({
title={t(`delete${isManagedEventPrefix()}_event_type`)}
confirmBtnText={t(`confirm_delete_event_type`)}
loadingText={t(`confirm_delete_event_type`)}
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
onConfirm={(e) => {
e.preventDefault();
deleteEventTypeHandler(deleteDialogTypeId);
@@ -856,7 +856,7 @@ const Main = ({
const searchParams = useCompatSearchParams();
const orgBranding = useOrgBranding();
if (!data || status === "loading") {
if (!data || status === "pending") {
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
const { data, status, error } = trpc.viewer.eventTypes.getByViewer.useQuery(filters && { filters }, {
refetchOnWindowFocus: false,
cacheTime: 1 * 60 * 60 * 1000,
gcTime: 1 * 60 * 60 * 1000,
staleTime: 1 * 60 * 60 * 1000,
});
@@ -36,7 +36,7 @@ const SkeletonLoader = ({ title, description }: { title: string; description: st
const ApiKeysView = () => {
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 [apiKeyToEdit, setApiKeyToEdit] = useState<(TApiKeys & { neverExpires?: boolean }) | undefined>(
@@ -57,7 +57,7 @@ const ApiKeysView = () => {
);
};
if (isLoading || !data) {
if (isPending || !data) {
return (
<SkeletonLoader
title={t("api_keys")}
@@ -317,7 +317,7 @@ const AppearanceView = ({
<SettingsToggle
toggleSwitchAtTheEnd={true}
title={t("disable_cal_branding", { appName: APP_NAME })}
disabled={!hasPaidPlan || mutation?.isLoading}
disabled={!hasPaidPlan || mutation?.isPending}
description={t("removes_cal_branding", { appName: APP_NAME })}
checked={hasPaidPlan ? hideBrandingValue : false}
Badge={<UpgradeTeamsBadge />}
@@ -332,12 +332,12 @@ const AppearanceView = ({
};
const AppearanceViewWrapper = () => {
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { isLoading: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
const { data: user, isPending } = trpc.viewer.me.useQuery();
const { isPending: isTeamPlanStatusLoading, hasPaidPlan } = useHasPaidPlan();
const { t } = useLocale();
if (isLoading || isTeamPlanStatusLoading || !user)
if (isPending || isTeamPlanStatusLoading || !user)
return <SkeletonLoader title={t("appearance")} description={t("appearance_description")} />;
return <AppearanceView user={user} hasPaidPlan={hasPaidPlan} />;
@@ -51,8 +51,8 @@ interface GeneralViewProps {
const GeneralQueryView = () => {
const { t } = useLocale();
const { data: user, isLoading } = trpc.viewer.me.useQuery();
if (isLoading) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
const { data: user, isPending } = trpc.viewer.me.useQuery();
if (isPending) return <SkeletonLoader title={t("general")} description={t("general_description")} />;
if (!user) {
throw new Error(t("something_went_wrong"));
}
@@ -233,7 +233,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
toggleSwitchAtTheEnd={true}
title={t("dynamic_booking")}
description={t("allow_dynamic_booking")}
disabled={mutation.isLoading}
disabled={mutation.isPending}
checked={isAllowDynamicBookingChecked}
onCheckedChange={(checked) => {
setIsAllowDynamicBookingChecked(checked);
@@ -246,7 +246,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
toggleSwitchAtTheEnd={true}
title={t("seo_indexing")}
description={t("allow_seo_indexing")}
disabled={mutation.isLoading}
disabled={mutation.isPending}
checked={isAllowSEOIndexingChecked}
onCheckedChange={(checked) => {
setIsAllowSEOIndexingChecked(checked);
@@ -259,7 +259,7 @@ const GeneralView = ({ localeProp, user }: GeneralViewProps) => {
toggleSwitchAtTheEnd={true}
title={t("monthly_digest_email")}
description={t("monthly_digest_email_for_teams")}
disabled={mutation.isLoading}
disabled={mutation.isPending}
checked={isReceiveMonthlyDigestEmailChecked}
onCheckedChange={(checked) => {
setIsReceiveMonthlyDigestEmailChecked(checked);
@@ -155,7 +155,7 @@ const OutOfOfficeSection = () => {
<Button
color="primary"
type="submit"
disabled={createOutOfOfficeEntry.isLoading}
disabled={createOutOfOfficeEntry.isPending}
data-testid="create-entry-ooo-redirect">
{t("create_entry")}
</Button>
@@ -168,7 +168,7 @@ const OutOfOfficeSection = () => {
const OutOfOfficeEntriesList = () => {
const { t } = useLocale();
const utils = trpc.useContext();
const { data, isLoading } = trpc.viewer.outOfOfficeEntriesList.useQuery();
const { data, isPending } = trpc.viewer.outOfOfficeEntriesList.useQuery();
const deleteOutOfOfficeEntryMutation = trpc.viewer.outOfOfficeEntryDelete.useMutation({
onSuccess: () => {
showToast(t("success_deleted_entry_out_of_office"), "success");
@@ -207,7 +207,7 @@ const OutOfOfficeEntriesList = () => {
type="button"
color="minimal"
variant="icon"
disabled={deleteOutOfOfficeEntryMutation.isLoading}
disabled={deleteOutOfOfficeEntryMutation.isPending}
StartIcon={Trash2}
onClick={() => {
deleteOutOfOfficeEntryMutation.mutate({ outOfOfficeUid: item.uuid });
@@ -216,7 +216,7 @@ const OutOfOfficeEntriesList = () => {
</TableCell>
</TableRow>
))}
{isLoading && (
{isPending && (
<TableRow>
{new Array(6).fill(0).map((_, index) => (
<TableCell key={index}>
@@ -226,7 +226,7 @@ const OutOfOfficeEntriesList = () => {
</TableRow>
)}
{!isLoading && (data === undefined || data.length === 0) && (
{!isPending && (data === undefined || data.length === 0) && (
<TableRow>
<TableCell colSpan={6} className="text-center">
<p className="text-subtle text-sm">{t("no_redirects_found")}</p>
@@ -84,10 +84,10 @@ const ProfileView = () => {
const { t } = useLocale();
const utils = trpc.useContext();
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, {
enabled: !isLoading && !user?.avatarUrl,
enabled: !isPending && !user?.avatarUrl,
});
const updateProfileMutation = trpc.viewer.updateProfile.useMutation({
@@ -222,7 +222,7 @@ const ProfileView = () => {
[ErrorCode.ThirdPartyIdentityProviderEnabled]: t("account_created_with_identity_provider"),
};
if (isLoading || !user) {
if (isPending || !user) {
return (
<SkeletonLoader title={t("profile")} description={t("profile_description", { appName: APP_NAME })} />
);
@@ -246,7 +246,7 @@ const ProfileView = () => {
<ProfileForm
key={JSON.stringify(defaultValues)}
defaultValues={defaultValues}
isLoading={updateProfileMutation.isLoading}
isPending={updateProfileMutation.isPending}
isFallbackImg={!user.avatarUrl && !avatarData?.avatar}
user={user}
userOrganization={user.organization}
@@ -354,7 +354,7 @@ const ProfileView = () => {
<DialogFooter showDivider>
<Button
color="primary"
loading={confirmPasswordMutation.isLoading}
loading={confirmPasswordMutation.isPending}
onClick={(e) => onConfirmPassword(e)}>
{t("confirm")}
</Button>
@@ -375,7 +375,7 @@ const ProfileView = () => {
<DialogFooter>
<Button
color="primary"
loading={updateProfileMutation.isLoading}
loading={updateProfileMutation.isPending}
onClick={(e) => onConfirmAuthEmailChange(e)}>
{t("confirm")}
</Button>
@@ -391,7 +391,7 @@ const ProfileForm = ({
defaultValues,
onSubmit,
extraField,
isLoading = false,
isPending = false,
isFallbackImg,
user,
userOrganization,
@@ -399,7 +399,7 @@ const ProfileForm = ({
defaultValues: FormValues;
onSubmit: (values: FormValues) => void;
extraField?: React.ReactNode;
isLoading: boolean;
isPending: boolean;
isFallbackImg: boolean;
user: RouterOutputs["viewer"]["me"];
userOrganization: RouterOutputs["viewer"]["me"]["organization"];
@@ -509,7 +509,7 @@ const ProfileForm = ({
</div>
</div>
<SectionBottomActions align="end">
<Button loading={isLoading} disabled={isDisabled} color="primary" type="submit">
<Button loading={isPending} disabled={isDisabled} color="primary" type="submit">
{t("update")}
</Button>
</SectionBottomActions>
@@ -69,7 +69,7 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
mutation.mutate({ disableImpersonation: !checked });
}}
switchContainerClassName="rounded-t-none border-t-0"
disabled={mutation.isLoading}
disabled={mutation.isPending}
/>
</div>
</>
@@ -77,10 +77,10 @@ const ProfileImpersonationView = ({ user }: { user: RouterOutputs["viewer"]["me"
};
const ProfileImpersonationViewWrapper = () => {
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: user, isPending } = trpc.viewer.me.useQuery();
const { t } = useLocale();
if (isLoading || !user)
if (isPending || !user)
return <SkeletonLoader title={t("impersonation")} description={t("impersonation_description")} />;
return <ProfileImpersonationView user={user} />;
@@ -215,9 +215,9 @@ const PasswordView = ({ user }: PasswordViewProps) => {
<Button
color="primary"
type="submit"
loading={passwordMutation.isLoading}
loading={passwordMutation.isPending}
onClick={() => formMethods.clearErrors("apiError")}
disabled={isDisabled || passwordMutation.isLoading || sessionMutation.isLoading}>
disabled={isDisabled || passwordMutation.isPending || sessionMutation.isPending}>
{t("update")}
</Button>
</SectionBottomActions>
@@ -268,7 +268,7 @@ const PasswordView = ({ user }: PasswordViewProps) => {
<SectionBottomActions align="end">
<Button
color="primary"
loading={sessionMutation.isLoading}
loading={sessionMutation.isPending}
onClick={() => {
sessionMutation.mutate({
metadata: { ...metadata, sessionTimeout },
@@ -277,8 +277,8 @@ const PasswordView = ({ user }: PasswordViewProps) => {
}}
disabled={
initialSessionTimeout === sessionTimeout ||
passwordMutation.isLoading ||
sessionMutation.isLoading
passwordMutation.isPending ||
sessionMutation.isPending
}>
{t("update")}
</Button>
@@ -293,9 +293,9 @@ const PasswordView = ({ user }: PasswordViewProps) => {
};
const PasswordViewWrapper = () => {
const { data: user, isLoading } = trpc.viewer.me.useQuery();
const { data: user, isPending } = trpc.viewer.me.useQuery();
const { t } = useLocale();
if (isLoading || !user)
if (isPending || !user)
return <SkeletonLoader title={t("password")} description={t("password_description")} />;
return <PasswordView user={user} />;
@@ -37,12 +37,12 @@ const TwoFactorAuthView = () => {
const utils = trpc.useContext();
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 [disableModalOpen, setDisableModalOpen] = useState<boolean>(false);
if (isLoading)
if (isPending)
return <SkeletonLoader title={t("2fa")} description={t("set_up_two_factor_authentication")} />;
const isCalProvider = user?.identityProvider === "CAL";
+1 -2
View File
@@ -19,7 +19,7 @@ import { useRouterQuery } from "@calcom/lib/hooks/useRouterQuery";
import useTheme from "@calcom/lib/hooks/useTheme";
import { collectPageParameters, telemetryEventTypes, useTelemetry } from "@calcom/lib/telemetry";
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 { useToggleQuery } from "@lib/hooks/useToggleQuery";
@@ -28,7 +28,6 @@ import type { inferSSRProps } from "@lib/types/inferSSRProps";
import PageWrapper from "@components/PageWrapper";
import Team from "@components/team/screens/Team";
import { UserAvatarGroup } from "@components/ui/avatar/UserAvatarGroup";
export { getServerSideProps };
+1 -1
View File
@@ -13,7 +13,7 @@ import { markdownToSafeHTML } from "@calcom/lib/markdownToSafeHTML";
import type { inferSSRProps } from "@calcom/types/inferSSRProps";
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";
@@ -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.",
"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.",
"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_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.",
@@ -302,6 +302,7 @@
"welcome_to_calcom": "ברוך הבא אל {{appName}}",
"welcome_instructions": "אמור לנו באיזה שם ברצונך שנקרא לך ובאיזה אזור זמן אתה נמצא. ניתן יהיה לערוך את הפרטים האלה מאוחר יותר.",
"connect_caldav": "חיבור לשרת CalDav (בטא)",
"connect_ics_feed": "חיבור לערוץ ICS",
"connect": "להתחבר",
"try_for_free": "לנסות בחינם",
"create_booking_link_with_calcom": "צור קישור תזמון משלך עם {{appName}}",
@@ -555,6 +556,7 @@
"enter_number_between_range": "נא להזין מספר בין 1 ל-{{maxOccurences}}",
"email_address": "כתובת דוא\"ל",
"enter_valid_email": "יש להזין כתובת דוא״ל תקינה",
"please_schedule_future_call": " נא לתזמן שיחה עתידית אם איננו זמינים תוך {{seconds}} שניות",
"location": "מיקום",
"address": "כתובת",
"enter_address": "יש להזין כתובת",
@@ -614,6 +616,7 @@
"new_member": "חבר/ה חדש/ה",
"invite": "להזמין",
"add_team_members": "הוספת חברי צוות",
"add_org_members": "הוספת חברים",
"add_team_members_description": "להזמין עוד אנשים להצטרף לצוות",
"add_team_member": "הוספת חבר צוות",
"invite_new_member": "להזמין חבר/ת צוות חדש/ה",
@@ -1978,6 +1981,7 @@
"connect_google_workspace": "חיבור Google Workspace",
"google_workspace_admin_tooltip": "עליך/ייך להיות אדמין/ית ב-Workspace כדי להשתמש בתכונה הזו",
"first_event_type_webhook_description": "צור/צרי את ה-Webhook הראשון שלך עבור סוג האירוע הזה",
"create_instant_meeting_webhook_description": "אפשר ליצור את ההתליה הראשונה שלך עם ‚נוצרה פגישה מיידית’ כהזנקה לסוג אירוע זה",
"install_app_on": "התקנת האפליקציה ב-",
"create_for": "צור/צרי עבור",
"currency": "מטבע",
@@ -2193,6 +2197,7 @@
"uprade_to_create_instant_bookings": "ניתן לשדג לרישיון התאגידי ולאפשר למשתמשים להצטרף לשיחה מיידית שמשתתפים יכולים לקפוץ ישירות אליה. זה מיועד רק לסוגי אירועים של צוותים",
"dont_want_to_wait": "לא רוצה להמתין?",
"meeting_started": "הפגישה החלה",
"pay_and_book": "נא לשלם כדי להזמין",
"booking_not_found_error": "לא ניתן למצוא הזמנה",
"booking_seats_full_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,
},
};
}
+2 -2
View File
@@ -4,7 +4,7 @@ import superjson from "superjson";
import { CALCOM_VERSION } from "@calcom/lib/constants";
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";
// 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 ssg = createProxySSGHelpers({
const ssg = createServerSideHelpers({
router: appRouter,
transformer: superjson,
ctx: {
+2 -2
View File
@@ -6,7 +6,7 @@ import { forms } from "@calcom/app-store/routing-forms/trpc/procedures/forms";
import { getLocale } from "@calcom/features/auth/lib/getLocale";
import { map } from "@calcom/features/flags/server/procedures/map";
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 { me } from "@calcom/trpc/server/routers/loggedInViewer/procedures/me";
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 i18n = await serverSideTranslations(locale, ["common", "vital"]);
const ssr = createProxySSGHelpers({
const ssr = createServerSideHelpers({
router: routerSlice,
transformer: superjson,
ctx: { ...ctx, locale, i18n },
@@ -71,7 +71,7 @@ export default function OmniInstallAppButton({
return (
<Button
loading={mutation.isLoading}
loading={mutation.isPending}
color="secondary"
className="[@media(max-width:260px)]:w-full [@media(max-width:260px)]:justify-center"
StartIcon={Plus}
+54 -51
View File
@@ -32,64 +32,67 @@ function useAddAppMutation(_type: App["type"] | null, allOptions?: UseAddAppMuta
AddAppMutationData,
Error,
{ type?: App["type"]; variant?: string; slug?: string; isOmniInstall?: boolean; teamId?: number } | ""
>(async (variables) => {
let type: string | null | undefined;
let isOmniInstall;
const teamId = variables && variables.teamId ? variables.teamId : undefined;
if (variables === "") {
type = _type;
} else {
isOmniInstall = variables.isOmniInstall;
type = variables.type;
}
if (type?.endsWith("_other_calendar")) {
type = type.split("_other_calendar")[0];
}
>({
...options,
mutationFn: async (variables) => {
let type: string | null | undefined;
let isOmniInstall;
const teamId = variables && variables.teamId ? variables.teamId : undefined;
if (variables === "") {
type = _type;
} else {
isOmniInstall = variables.isOmniInstall;
type = variables.type;
}
if (type?.endsWith("_other_calendar")) {
type = type.split("_other_calendar")[0];
}
if (options?.installGoogleVideo && type !== "google_calendar")
throw new Error("Could not install Google Meet");
if (options?.installGoogleVideo && type !== "google_calendar")
throw new Error("Could not install Google Meet");
const state: IntegrationOAuthCallbackState = {
returnTo:
returnTo ||
WEBAPP_URL +
getInstalledAppPath(
{ variant: variables && variables.variant, slug: variables && variables.slug },
location.search
),
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
...(teamId && { teamId }),
};
const state: IntegrationOAuthCallbackState = {
returnTo:
returnTo ||
WEBAPP_URL +
getInstalledAppPath(
{ variant: variables && variables.variant, slug: variables && variables.slug },
location.search
),
...(type === "google_calendar" && { installGoogleVideo: options?.installGoogleVideo }),
...(teamId && { teamId }),
};
const stateStr = encodeURIComponent(JSON.stringify(state));
const searchParams = `?state=${stateStr}${teamId ? `&teamId=${teamId}` : ""}`;
const stateStr = encodeURIComponent(JSON.stringify(state));
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) {
const errorBody = await res.json();
throw new Error(errorBody.message || "Something went wrong");
}
if (!res.ok) {
const errorBody = await res.json();
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") };
}
// 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;
}
@@ -121,7 +121,7 @@ function AlbySetupPage(props: IAlbySetupProps) {
});
}, [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" />;
}
@@ -15,22 +15,26 @@ const EventTypeAppCard: EventTypeAppCardComponent = function EventTypeAppCard({
const [selectedProject, setSelectedProject] = useState<undefined | { label: string; value: string }>();
const { data } = trpc.viewer.appBasecamp3.projects.useQuery();
const setProject = trpc.viewer.appBasecamp3.projectMutation.useMutation();
useEffect(() => {
setSelectedProject({
value: data?.projects.currentProject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
});
setProjects(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?.projects?.map((project: any) => {
return {
value: project.id,
label: project.name,
};
})
);
}, [data]);
useEffect(
function refactorMeWithoutEffect() {
setSelectedProject({
value: data?.projects.currentProject,
// eslint-disable-next-line @typescript-eslint/no-explicit-any
label: data?.projects?.find((project: any) => project.id === data?.currentProject)?.name,
});
setProjects(
// eslint-disable-next-line @typescript-eslint/no-explicit-any
data?.projects?.map((project: any) => {
return {
value: project.id,
label: project.name,
};
})
);
},
[data]
);
return (
<AppCard
+3 -3
View File
@@ -34,7 +34,7 @@ export const InstallAppButtonWithoutPlanCheck = (
onClick: () => {
mutation.mutate({ type: props.type });
},
loading: mutation.isLoading,
loading: mutation.isPending,
})}
</>
);
@@ -56,10 +56,10 @@ export const InstallAppButton = (
disableInstall?: boolean;
} & InstallAppButtonProps
) => {
const { isLoading: isUserLoading, data: user } = trpc.viewer.me.useQuery();
const { isPending: isUserLoading, data: user } = trpc.viewer.me.useQuery();
const router = useRouter();
const proProtectionElementRef = useRef<HTMLDivElement | null>(null);
const { isLoading: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
const { isPending: isTeamPlanStatusLoading, hasTeamPlan } = useHasTeamPlan();
useEffect(() => {
const el = proProtectionElementRef.current;
+3 -7
View File
@@ -1,18 +1,14 @@
---
items:
- 1.png
- 1.jpeg
- 2.png
- 3.png
- 4.png
- 5.png
---
{DESCRIPTION}
### It’s time.
### Cron's most valuable features
<!-- From "FAQs: What are Cron’s most valuable features?"
https://cronhq.notion.site/Cron-Calendar-5625be54feac4e13a75b10271b65ddb7 -->
All of your commitments, now in one place. Meet the beautifully designed, fully integrated calendar for your work and life.
- **Availability sharing** — mark holds on the grid, share the slots, and schedule with ease.
- **Team overlays** — quickly overlay teammates' calendars.
+4 -4
View File
@@ -1,15 +1,15 @@
{
"/*": "Don't modify slug - If required, do it using cli edit command",
"name": "Cron",
"name": "Notion Calendar",
"slug": "cron",
"type": "cron_other",
"type": "cron_calendar",
"logo": "logo.png",
"url": "https://github.com/miguelnietoa",
"variant": "other",
"variant": "calendar",
"categories": ["calendar"],
"publisher": "Miguel Nieto",
"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,
"__createdUsingCli": true,
"__template": "link-as-an-app",
+1 -1
View File
@@ -10,5 +10,5 @@
"devDependencies": {
"@calcom/types": "*"
},
"description": "Cron is the next-generation calendar for professionals and teams."
"description": "All of your commitments, now in one place."
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 173 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 367 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 487 KiB

After

Width:  |  Height:  |  Size: 477 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1000 KiB

After

Width:  |  Height:  |  Size: 131 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 182 KiB

After

Width:  |  Height:  |  Size: 114 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 73 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 36 KiB

After

Width:  |  Height:  |  Size: 61 KiB

@@ -11,7 +11,7 @@ export default function InstallAppButton(props: InstallAppButtonProps) {
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
loading: mutation.isPending,
})}
</>
);
@@ -9,7 +9,7 @@ export default function InstallAppButton(props: InstallAppButtonProps) {
onClick() {
mutation.mutate("");
},
loading: mutation.isLoading,
loading: mutation.isPending,
})}
</>
);
@@ -23,15 +23,15 @@ export const SearchDialog = (props: ISearchDialog) => {
const [nextOffset, setNextOffset] = useState<number>(0);
const [keyword, setKeyword] = useState<string>("");
const { isOpenDialog, setIsOpenDialog } = props;
const [isLoading, setIsLoading] = useState(false);
const [isPending, setIsPending] = useState(false);
const [errorMessage, setErrorMessage] = useState("");
const [selectedMode, setSelectedMode] = useState<Mode>(MODE_SEARCH);
const searchGiphy = async (keyword: string, offset: number) => {
if (isLoading) {
if (isPending) {
return;
}
setIsLoading(true);
setIsPending(true);
setErrorMessage("");
const res = await fetch("/api/integrations/giphy/search", {
method: "POST",
@@ -53,15 +53,15 @@ export const SearchDialog = (props: ISearchDialog) => {
setErrorMessage("No Result found");
}
}
setIsLoading(false);
setIsPending(false);
return null;
};
const getGiphyByUrl = async (url: string) => {
if (isLoading) {
if (isPending) {
return;
}
setIsLoading(true);
setIsPending(true);
setErrorMessage("");
const res = await fetch("/api/integrations/giphy/get", {
method: "POST",
@@ -81,7 +81,7 @@ export const SearchDialog = (props: ISearchDialog) => {
setErrorMessage("No Result found");
}
}
setIsLoading(false);
setIsPending(false);
return null;
};
@@ -139,14 +139,14 @@ export const SearchDialog = (props: ISearchDialog) => {
}}
/>
</div>
<Button type="submit" tabIndex={-1} color="secondary" loading={isLoading}>
<Button type="submit" tabIndex={-1} color="secondary" loading={isPending}>
{t("search")}
</Button>
</form>
{gifImage && (
<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">
{isLoading ? (
{isPending ? (
<div className="flex h-[200px] w-full items-center justify-center bg-gray-400 pb-3 pt-3">
<svg
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"
color="secondary"
type="button"
loading={isLoading}
loading={isPending}
onClick={() => searchGiphy(keyword, nextOffset)}>
Shuffle
</Button>
@@ -59,7 +59,7 @@ export default function MakeSetup({ inviteLink }: InferGetServerSidePropsType<ty
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" />;
}
@@ -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" />;
}
@@ -147,7 +147,7 @@ function NewFormDialog({ appUrl }: { appUrl: string }) {
</div>
<DialogFooter showDivider className="mt-12">
<DialogClose />
<Button loading={mutation.isLoading} data-testid="add-form" type="submit">
<Button loading={mutation.isPending} data-testid="add-form" type="submit">
{t("continue")}
</Button>
</DialogFooter>
@@ -234,7 +234,7 @@ function Dialogs({
<RoutingFormEmbedDialog />
<Dialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
<ConfirmationDialogContent
isLoading={deleteMutation.isLoading}
isPending={deleteMutation.isPending}
variety="danger"
title={t("delete_form")}
confirmBtnText={t("delete_form_action")}
@@ -264,12 +264,12 @@ const actionsCtx = createContext({
_delete: {
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
onAction: (_arg: { routingForm: RoutingForm | null }) => {},
isLoading: false,
isPending: false,
},
toggle: {
// eslint-disable-next-line @typescript-eslint/no-empty-function, @typescript-eslint/no-unused-vars
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);
setDeleteDialogFormId(routingForm.id);
},
isLoading: false,
isPending: false,
},
toggle: {
onAction: ({ routingForm, checked }) => {
@@ -344,7 +344,7 @@ export function FormActionsProvider({ appUrl, children }: { appUrl: string; chil
disabled: !checked,
});
},
isLoading: toggleMutation.isLoading,
isPending: toggleMutation.isPending,
},
}}>
{children}
@@ -446,7 +446,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
},
_delete: {
onClick: () => _delete.onAction({ routingForm }),
loading: _delete.isLoading,
loading: _delete.isPending,
},
create: {
onClick: () => openModal({ action: "new", target: "" }),
@@ -479,7 +479,7 @@ export const FormAction = forwardRef(function FormAction<T extends typeof Button
</div>
);
},
loading: toggle.isLoading,
loading: toggle.isPending,
},
};
@@ -65,7 +65,7 @@ const Actions = ({
}: {
form: RoutingFormWithResponseCount;
mutation: {
isLoading: boolean;
isPending: boolean;
};
}) => {
const { t } = useLocale();
@@ -218,7 +218,7 @@ const Actions = ({
</FormActionsDropdown>
</div>
<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")}
</Button>
</div>
@@ -518,7 +518,7 @@ function SingleForm({ form, appUrl, Page }: 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 },
{
initialData: _form,
@@ -527,7 +527,7 @@ export default function SingleFormWrapper({ form: _form, ...props }: SingleFormC
);
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
return null;
}
@@ -34,7 +34,7 @@ type QueryBuilderUpdatedConfig = typeof QueryBuilderInitialConfig & { fields: Co
const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: JsonLogicQuery | null }) => {
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(
{
formId: formId,
@@ -57,7 +57,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
const headers = useRef<string[] | null>(null);
if (!isLoading && !data) {
if (!isPending && !data) {
return <div>Error loading report {error?.message} </div>;
}
headers.current = (data?.pages && data?.pages[0]?.headers) || headers.current;
@@ -81,7 +81,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
</th>
))}
</tr>
{!isLoading &&
{!isPending &&
data?.pages.map((page) => {
return page.responses?.map((responses, rowIndex) => {
const isLastRow = page.responses.length - 1 === rowIndex;
@@ -113,7 +113,7 @@ const Result = ({ formId, jsonLogicQuery }: { formId: string; jsonLogicQuery: Js
});
})}
</table>
{isLoading ? <div className="text-default p-2">{t("loading")}</div> : ""}
{isPending ? <div className="text-default p-2">{t("loading")}</div> : ""}
{hasNextPage && (
<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">
<Button
className="dark:bg-darkmodebrand dark:text-darkmodebrandcontrast dark:hover:border-darkmodebrandcontrast dark:border-transparent"
loading={responseMutation.isLoading}
loading={responseMutation.isPending}
type="submit"
color="primary">
{t("submit")}
+1 -1
View File
@@ -35,7 +35,7 @@ export interface InstallAppButtonProps {
renderProps: ButtonProps & {
/** Tells that the default render component should be used */
useDefaultComponent?: boolean;
isLoading?: boolean;
isPending?: boolean;
}
) => JSX.Element;
onChanged?: () => unknown;
@@ -44,7 +44,7 @@ const wipeMyCalAction = async (props: IWipeMyCalAction) => {
export const ConfirmDialog = (props: IConfirmDialogWipe) => {
const { t } = useLocale();
const { isOpenDialog, setIsOpenDialog } = props;
const [isLoading, setIsLoading] = useState(false);
const [isPending, setIsPending] = useState(false);
const today = dayjs();
const initialDate = today.startOf("day");
const endDate = today.endOf("day");
@@ -52,9 +52,9 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
const utils = trpc.useContext();
const rescheduleApi = useMutation(
async () => {
setIsLoading(true);
const rescheduleApi = useMutation({
mutationFn: async () => {
setIsPending(true);
try {
const result = await wipeMyCalAction({
initialDate: initialDate.toISOString(),
@@ -68,14 +68,12 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
showToast(t("unexpected_error_try_again"), "error");
// @TODO: notify
}
setIsLoading(false);
setIsPending(false);
},
{
async onSettled() {
await utils.viewer.bookings.invalidate();
},
}
);
async onSettled() {
await utils.viewer.bookings.invalidate();
},
});
return (
<Dialog open={isOpenDialog} onOpenChange={setIsOpenDialog}>
@@ -104,7 +102,7 @@ export const ConfirmDialog = (props: IConfirmDialogWipe) => {
<Button
color="primary"
data-testid="send_request"
disabled={isLoading}
disabled={isPending}
onClick={async () => {
try {
rescheduleApi.mutate();
@@ -13,7 +13,7 @@ interface IWipeMyCalActionButtonProps {
const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
const { bookingsEmpty, bookingStatus } = props;
const [openDialog, setOpenDialog] = useState(false);
const { isSuccess, isLoading, data } = trpc.viewer.integrations.useQuery({
const { isSuccess, isPending, data } = trpc.viewer.integrations.useQuery({
variant: "other",
onlyInstalled: undefined,
});
@@ -27,7 +27,7 @@ const WipeMyCalActionButton = (props: IWipeMyCalActionButtonProps) => {
return (
<>
{data && isSuccess && !isLoading && credentialId && (
{data && isSuccess && !isPending && credentialId && (
<div className="mb-4">
<ConfirmDialog isOpenDialog={openDialog} setIsOpenDialog={setOpenDialog} />
<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 });
}
if (integrations.isLoading) {
if (integrations.isPending) {
return <div className="bg-emphasis absolute z-50 flex h-screen w-full items-center" />;
}
+2 -2
View File
@@ -270,7 +270,7 @@ const AdminAppsListContainer = () => {
const { t } = useLocale();
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 },
{ enabled: searchParams !== null }
);
@@ -291,7 +291,7 @@ const AdminAppsListContainer = () => {
const handleModelOpen = (data: EditModalState) => setModalState({ ...data });
if (isLoading) return <SkeletonLoader />;
if (isPending) return <SkeletonLoader />;
if (!apps || apps.length === 0) {
return (
@@ -2,7 +2,7 @@ import jwt from "jsonwebtoken";
import type { NextApiRequest } from "next";
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[] = []) {
const token = req.headers.authorization?.split(" ")[1] || "";
+4 -4
View File
@@ -207,7 +207,7 @@ const BookerComponent = ({
} = useCalendars();
useEffect(() => {
if (event.isLoading) return setBookerState("loading");
if (event.isPending) return setBookerState("loading");
if (!selectedDate) return setBookerState("selecting_date");
if (!selectedTimeslot) return setBookerState("selecting_time");
return setBookerState("booking");
@@ -399,7 +399,7 @@ const BookerComponent = ({
<BookerSection
area="meta"
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 === "mobile" && bookerState === "booking") && (
<div className="mt-auto px-5 py-3 ">
@@ -434,7 +434,7 @@ const BookerComponent = ({
visible={layout === BookerLayouts.WEEK_VIEW}
className="border-subtle sticky top-0 ml-[-1px] h-full md:border-l"
{...fadeInLeft}>
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isLoading} />
<LargeCalendar extraDays={extraDays} schedule={schedule.data} isLoading={schedule.isPending} />
</BookerSection>
<BookerSection
@@ -456,7 +456,7 @@ const BookerComponent = ({
extraDays={extraDays}
limitHeight={layout === BookerLayouts.MONTH_VIEW}
schedule={schedule?.data}
isLoading={schedule.isLoading}
isLoading={schedule.isPending}
seatsPerTimeSlot={event.data?.seatsPerTimeSlot}
showAvailableSeatsCount={event.data?.seatsShowAvailabilityCount}
/>
@@ -63,7 +63,7 @@ export const BookEventForm = ({
}, [eventType]);
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)
return (
<EmptyScreen
@@ -27,7 +27,7 @@ export const DatePicker = ({
return (
<DatePickerComponent
isLoading={schedule.isLoading}
isPending={schedule.isPending}
onChange={(date: Dayjs | null) => {
setSelectedDate(date === null ? date : date.format("YYYY-MM-DD"));
}}
@@ -24,10 +24,10 @@ const TimezoneSelect = dynamic(
export const EventMeta = ({
event,
isLoading,
isPending,
}: {
event: useEventReturnType["data"];
isLoading: useEventReturnType["isLoading"];
isPending: useEventReturnType["isPending"];
}) => {
const { setTimezone, timeFormat, timezone } = useTimePreferences();
const selectedDuration = useBookerStore((state) => state.selectedDuration);
@@ -40,7 +40,6 @@ export const EventMeta = ({
shallow
);
const { i18n, t } = useLocale();
const embedUiConfig = useEmbedUiConfig();
const isEmbed = useIsEmbed();
const hideEventTypeDetails = isEmbed ? embedUiConfig.hideEventTypeDetails : false;
@@ -66,12 +65,12 @@ export const EventMeta = ({
return (
<div className="relative z-10 p-6" data-testid="event-meta">
{isLoading && (
{isPending && (
<m.div {...fadeInUp} initial="visible" layout>
<EventMetaSkeleton />
</m.div>
)}
{!isLoading && !!event && (
{!isPending && !!event && (
<m.div {...fadeInUp} layout transition={{ ...fadeInUp.transition, delay: 0.3 }}>
<EventMembers
schedulingType={event.schedulingType}
@@ -1,8 +1,7 @@
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { SchedulingType } from "@calcom/prisma/enums";
import { type RouterOutputs } from "@calcom/trpc";
import { Button } from "@calcom/ui";
import { UserAvatarGroupWithOrg } from "@calcom/web/components/ui/avatar/UserAvatarGroupWithOrg";
import { Button, UserAvatarGroupWithOrg } from "@calcom/ui";
interface IInstantBookingProps {
onConnectNow: () => void;
@@ -74,7 +74,7 @@ export const LargeCalendar = ({
return (
<div className="h-full [--calendar-dates-sticky-offset:66px]">
<Calendar
isLoading={isLoading}
isPending={isLoading}
availableTimeslots={availableSlots}
startHour={0}
endHour={23}
@@ -1,6 +1,6 @@
import { useMutation } from "@tanstack/react-query";
import { useRouter, useSearchParams } from "next/navigation";
import { useRef, useState } from "react";
import { useRef, useState, useEffect } from "react";
import { createPaymentLink } from "@calcom/app-store/stripepayment/lib/client";
import dayjs from "@calcom/dayjs";
@@ -74,27 +74,33 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
enabled: !!bookingId && !hasInstantMeetingTokenExpired,
refetchInterval: 2000,
refetchIntervalInBackground: true,
onSuccess: (data) => {
try {
showToast(t("something_went_wrong_on_our_end"), "error");
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
data.booking?.metadata || {}
)?.videoCallUrl;
if (locationVideoCallUrl) {
router.push(locationVideoCallUrl);
} else {
showToast(t("something_went_wrong_on_our_end"), "error");
}
} catch (err) {
showToast(t("something_went_wrong_on_our_end"), "error");
}
},
}
);
useEffect(
function refactorMeWithoutEffect() {
const data = _instantBooking.data;
if (!data) return;
try {
showToast(t("something_went_wrong_on_our_end"), "error");
const createBookingMutation = useMutation(createBooking, {
const locationVideoCallUrl: string | undefined = bookingMetadataSchema.parse(
data.booking?.metadata || {}
)?.videoCallUrl;
if (locationVideoCallUrl) {
router.push(locationVideoCallUrl);
} else {
showToast(t("something_went_wrong_on_our_end"), "error");
}
} catch (err) {
showToast(t("something_went_wrong_on_our_end"), "error");
}
},
[_instantBooking.data]
);
const createBookingMutation = useMutation({
mutationFn: createBooking,
onSuccess: (responseData) => {
const { uid, paymentUid } = responseData;
const fullName = getFullName(bookingForm.getValues("responses.name"));
@@ -141,7 +147,8 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
},
});
const createInstantBookingMutation = useMutation(createInstantBooking, {
const createInstantBookingMutation = useMutation({
mutationFn: createInstantBooking,
onSuccess: (responseData) => {
updateQueryParam("bookingId", responseData.bookingId);
setExpiryTime(responseData.expires);
@@ -153,7 +160,8 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
},
});
const createRecurringBookingMutation = useMutation(createRecurringBooking, {
const createRecurringBookingMutation = useMutation({
mutationFn: createRecurringBooking,
onSuccess: async (responseData) => {
const booking = responseData[0] || {};
const { uid } = booking;
@@ -253,10 +261,10 @@ export const useBookings = ({ event, hashedLink, bookingForm }: IUseBookings) =>
// A redirect is triggered on mutation success, so keep the loading state while it is happening.
const loadingStates = {
creatingBooking: createBookingMutation.isLoading || createBookingMutation.isSuccess,
creatingBooking: createBookingMutation.isPending || createBookingMutation.isSuccess,
creatingRecurringBooking:
createRecurringBookingMutation.isLoading || createRecurringBookingMutation.isSuccess,
creatingInstantBooking: createInstantBookingMutation.isLoading,
createRecurringBookingMutation.isPending || createRecurringBookingMutation.isSuccess,
creatingInstantBooking: createInstantBookingMutation.isPending,
};
return {
@@ -1,5 +1,6 @@
import { useSession } from "next-auth/react";
import { useSearchParams } from "next/navigation";
import { useEffect } from "react";
import { shallow } from "zustand/shallow";
import { useTimePreferences } from "@calcom/features/bookings/lib";
@@ -30,7 +31,7 @@ export const useCalendars = () => {
shallow
);
const { data: overlayBusyDates } = trpc.viewer.availability.calendarOverlay.useQuery(
const { data: overlayBusyDates, isError } = trpc.viewer.availability.calendarOverlay.useQuery(
{
loggedInUsersTz: timezone || "Europe/London",
dateFrom: selectedDate,
@@ -42,13 +43,18 @@ export const useCalendars = () => {
},
{
enabled: !!session && set.size > 0 && switchEnabled,
onError: () => {
clearSet();
},
}
);
const { data, isLoading } = trpc.viewer.connectedCalendars.useQuery(undefined, {
useEffect(
function refactorMeWithoutEffect() {
if (!isError) return;
clearSet();
},
[isError]
);
const { data, isPending } = trpc.viewer.connectedCalendars.useQuery(undefined, {
enabled: !!calendarSettingsOverlay || Boolean(searchParams?.get("overlayCalendar")),
});
@@ -56,7 +62,7 @@ export const useCalendars = () => {
overlayBusyDates,
isOverlayCalendarEnabled: switchEnabled,
connectedCalendars: data?.connectedCalendars || [],
loadingConnectedCalendar: isLoading,
loadingConnectedCalendar: isPending,
onToggleCalendar: () => {
utils.viewer.availability.calendarOverlay.reset();
},

Some files were not shown because too many files have changed in this diff Show More