Fix/availability not loading (#7789)
* Set default schedule if there is not one * Removing last schedule sets user.defaultScheduleId to null * Create new schedule from event type * First schedule created is the default * Go back if new schedule was created * Moved update logic out of getter * Remove flashing of empty screen * Add border prop to EmptyScreen * User requires a schedule * Change back schema --------- Co-authored-by: Peer Richelsen <peeroke@gmail.com>
This commit is contained in:
co-authored by
Peer Richelsen
parent
867966dd75
commit
a2457ab619
@@ -4,13 +4,15 @@ import type { OptionProps, SingleValueProps } from "react-select";
|
||||
import { components } from "react-select";
|
||||
|
||||
import dayjs from "@calcom/dayjs";
|
||||
import { NewScheduleButton } from "@calcom/features/schedules";
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { weekdayNames } from "@calcom/lib/weekday";
|
||||
import type { RouterOutputs } from "@calcom/trpc/react";
|
||||
import { trpc } from "@calcom/trpc/react";
|
||||
import useMeQuery from "@calcom/trpc/react/hooks/useMeQuery";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe } from "@calcom/ui/components/icon";
|
||||
import { Badge, Button, Select, SettingsToggle, SkeletonText, EmptyScreen } from "@calcom/ui";
|
||||
import { FiExternalLink, FiGlobe, FiClock } from "@calcom/ui/components/icon";
|
||||
|
||||
import { SelectSkeletonLoader } from "@components/availability/SkeletonLoader";
|
||||
|
||||
@@ -52,23 +54,24 @@ const SingleValue = ({ ...props }: SingleValueProps<AvailabilityOption>) => {
|
||||
|
||||
const AvailabilitySelect = ({
|
||||
className = "",
|
||||
isLoading,
|
||||
schedules,
|
||||
...props
|
||||
}: {
|
||||
className?: string;
|
||||
name: string;
|
||||
value: number;
|
||||
isLoading: boolean;
|
||||
schedules: RouterOutputs["viewer"]["availability"]["list"]["schedules"] | [];
|
||||
onBlur: () => void;
|
||||
onChange: (value: AvailabilityOption | null) => void;
|
||||
}) => {
|
||||
const { data, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||
const { t } = useLocale();
|
||||
|
||||
if (isLoading) {
|
||||
return <SelectSkeletonLoader />;
|
||||
}
|
||||
|
||||
const schedules = data?.schedules || [];
|
||||
|
||||
const options = schedules.map((schedule) => ({
|
||||
value: schedule.id,
|
||||
label: schedule.name,
|
||||
@@ -107,6 +110,7 @@ const EventTypeScheduleDetails = () => {
|
||||
const { watch } = useFormContext<FormValues>();
|
||||
|
||||
const scheduleId = watch("schedule");
|
||||
|
||||
const { isLoading, data: schedule } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId: scheduleId || loggedInUser?.defaultScheduleId || undefined },
|
||||
{ enabled: !!scheduleId || !!loggedInUser?.defaultScheduleId }
|
||||
@@ -174,6 +178,19 @@ const EventTypeScheduleDetails = () => {
|
||||
|
||||
const EventTypeSchedule = () => {
|
||||
const { t } = useLocale();
|
||||
const { data: schedules, isLoading } = trpc.viewer.availability.list.useQuery();
|
||||
|
||||
if (!schedules?.schedules.length && !isLoading)
|
||||
return (
|
||||
<EmptyScreen
|
||||
Icon={FiClock}
|
||||
headline={t("new_schedule_heading")}
|
||||
description={t("new_schedule_description")}
|
||||
buttonRaw={<NewScheduleButton fromEventType />}
|
||||
border={false}
|
||||
/>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
@@ -183,14 +200,18 @@ const EventTypeSchedule = () => {
|
||||
<Controller
|
||||
name="schedule"
|
||||
render={({ field }) => (
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
<>
|
||||
<AvailabilitySelect
|
||||
value={field.value}
|
||||
onBlur={field.onBlur}
|
||||
name={field.name}
|
||||
schedules={schedules?.schedules || []}
|
||||
isLoading={isLoading}
|
||||
onChange={(selected) => {
|
||||
field.onChange(selected?.value || null);
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -94,6 +94,7 @@ export default function Availability() {
|
||||
data: { schedule: scheduleId },
|
||||
} = useTypedQuery(querySchema);
|
||||
|
||||
const { fromEventType } = router.query;
|
||||
const { timeFormat } = me.data || { timeFormat: null };
|
||||
const { data: schedule, isLoading } = trpc.viewer.availability.schedule.get.useQuery(
|
||||
{ scheduleId },
|
||||
@@ -153,7 +154,7 @@ export default function Availability() {
|
||||
|
||||
return (
|
||||
<Shell
|
||||
backPath="/availability"
|
||||
backPath={fromEventType ? true : "/availability"}
|
||||
title={schedule?.name ? schedule.name + " | " + t("availability") : t("availability")}
|
||||
heading={
|
||||
<Controller
|
||||
@@ -200,7 +201,14 @@ export default function Availability() {
|
||||
<VerticalDivider />
|
||||
<Dialog>
|
||||
<DialogTrigger asChild>
|
||||
<Button StartIcon={FiTrash} variant="icon" color="destructive" aria-label={t("delete")} />
|
||||
<Button
|
||||
StartIcon={FiTrash}
|
||||
variant="icon"
|
||||
color="destructive"
|
||||
aria-label={t("delete")}
|
||||
disabled={schedule?.isLastSchedule}
|
||||
tooltip={t("requires_at_least_one_schedule")}
|
||||
/>
|
||||
</DialogTrigger>
|
||||
<ConfirmationDialogContent
|
||||
isLoading={deleteMutation.isLoading}
|
||||
|
||||
@@ -93,7 +93,13 @@ export function AvailabilityList({ schedules }: RouterOutputs["viewer"]["availab
|
||||
key={schedule.id}
|
||||
schedule={schedule}
|
||||
updateDefault={updateMutation.mutate}
|
||||
deleteFunction={deleteMutation.mutate}
|
||||
deleteFunction={() => {
|
||||
if (schedules.length === 1) {
|
||||
showToast(t("requires_at_least_one_schedule"), "error");
|
||||
} else {
|
||||
deleteMutation.mutate;
|
||||
}
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -1668,6 +1668,7 @@
|
||||
"invalid_event_name_variables": "There is an invalid variable in your event name",
|
||||
"select_all": "Select All",
|
||||
"default_conferencing_bulk_title": "Bulk update existing event types",
|
||||
"requires_at_least_one_schedule": "You are required to have at least one schedule",
|
||||
"default_conferencing_bulk_description": "Update the locations for the selected event types",
|
||||
"looking_for_more_analytics": "Looking for more analytics?",
|
||||
"add_filter": "Add filter",
|
||||
|
||||
@@ -17,7 +17,13 @@ import {
|
||||
} from "@calcom/ui";
|
||||
import { FiPlus } from "@calcom/ui/components/icon";
|
||||
|
||||
export function NewScheduleButton({ name = "new-schedule" }: { name?: string }) {
|
||||
export function NewScheduleButton({
|
||||
name = "new-schedule",
|
||||
fromEventType,
|
||||
}: {
|
||||
name?: string;
|
||||
fromEventType?: boolean;
|
||||
}) {
|
||||
const router = useRouter();
|
||||
const { t } = useLocale();
|
||||
|
||||
@@ -29,7 +35,7 @@ export function NewScheduleButton({ name = "new-schedule" }: { name?: string })
|
||||
|
||||
const createMutation = trpc.viewer.availability.schedule.create.useMutation({
|
||||
onSuccess: async ({ schedule }) => {
|
||||
await router.push("/availability/" + schedule.id);
|
||||
await router.push(`/availability/${schedule.id}${fromEventType ? "?fromEventType=true" : ""}`);
|
||||
showToast(t("schedule_created_successfully", { scheduleName: schedule.name }), "success");
|
||||
utils.viewer.availability.list.setData(undefined, (data) => {
|
||||
const newSchedule = { ...schedule, isDefault: false, availability: [] };
|
||||
|
||||
@@ -63,7 +63,7 @@ import {
|
||||
FiSettings,
|
||||
FiSlack,
|
||||
FiUsers,
|
||||
FiZap
|
||||
FiZap,
|
||||
} from "@calcom/ui/components/icon";
|
||||
|
||||
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
|
||||
|
||||
@@ -2,11 +2,8 @@ import Image from "next/image";
|
||||
import type { ReactNode } from "react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import { IS_SELF_HOSTED } from "@calcom/lib/constants";
|
||||
import { useHasTeamPlan } from "@calcom/lib/hooks/useHasPaidPlan";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
import { EmptyScreen } from "@calcom/ui";
|
||||
import { FiUsers } from "@calcom/ui/components/icon";
|
||||
|
||||
export function UpgradeTip({
|
||||
dark,
|
||||
@@ -42,9 +39,16 @@ export function UpgradeTip({
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="relative flex justify-between w-full pb-10 min-h-[295px] items-center rounded-lg overflow-hidden">
|
||||
<Image alt={title} src={background} className="absolute object-cover min-h-[295px] w-full rounded-lg" height={295} width={1118} quality={100} />
|
||||
<div className="relative px-8 mt-4 sm:px-14">
|
||||
<div className="relative flex min-h-[295px] w-full items-center justify-between overflow-hidden rounded-lg pb-10">
|
||||
<Image
|
||||
alt={title}
|
||||
src={background}
|
||||
className="absolute min-h-[295px] w-full rounded-lg object-cover"
|
||||
height={295}
|
||||
width={1118}
|
||||
quality={100}
|
||||
/>
|
||||
<div className="relative mt-4 px-8 sm:px-14">
|
||||
<h1 className={classNames("font-cal text-3xl", dark && "text-white")}>{t(title)}</h1>
|
||||
<p className={classNames("mt-4 mb-8 max-w-sm", dark ? "text-white" : "text-gray-700")}>
|
||||
{t(description)}
|
||||
@@ -53,11 +57,11 @@ export function UpgradeTip({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid-cols-3 mt-4 md:grid md:gap-4">
|
||||
<div className="mt-4 grid-cols-3 md:grid md:gap-4">
|
||||
{features.map((feature) => (
|
||||
<div key={feature.title} className="mb-4 min-h-[180px] w-full rounded-md bg-gray-50 p-8 md:mb-0">
|
||||
{feature.icon}
|
||||
<h2 className="mt-4 text-lg font-cal">{feature.title}</h2>
|
||||
<h2 className="font-cal mt-4 text-lg">{feature.title}</h2>
|
||||
<p className="text-gray-700">{feature.description}</p>
|
||||
</div>
|
||||
))}
|
||||
|
||||
@@ -38,6 +38,16 @@ export const availabilityRouter = router({
|
||||
});
|
||||
|
||||
const defaultScheduleId = await getDefaultScheduleId(user.id, prisma);
|
||||
if (!user.defaultScheduleId) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
defaultScheduleId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return {
|
||||
schedules: schedules.map((schedule) => ({
|
||||
@@ -93,6 +103,12 @@ export const availabilityRouter = router({
|
||||
});
|
||||
}
|
||||
const timeZone = schedule.timeZone || user.timeZone;
|
||||
|
||||
const schedulesCount = await ctx.prisma.schedule.count({
|
||||
where: {
|
||||
userId: ctx.user.id,
|
||||
},
|
||||
});
|
||||
return {
|
||||
id: schedule.id,
|
||||
name: schedule.name,
|
||||
@@ -139,6 +155,7 @@ export const availabilityRouter = router({
|
||||
return acc;
|
||||
}, [] as { ranges: TimeRange[] }[]),
|
||||
isDefault: !input.scheduleId || user.defaultScheduleId === schedule.id,
|
||||
isLastSchedule: schedulesCount <= 1,
|
||||
};
|
||||
}),
|
||||
create: authedProcedure
|
||||
@@ -201,9 +218,16 @@ export const availabilityRouter = router({
|
||||
const schedule = await prisma.schedule.create({
|
||||
data,
|
||||
});
|
||||
const hasDefaultScheduleId = await hasDefaultSchedule(user, prisma);
|
||||
if (!hasDefaultScheduleId) {
|
||||
await setupDefaultSchedule(user.id, schedule.id, prisma);
|
||||
|
||||
if (!user.defaultScheduleId) {
|
||||
await prisma.user.update({
|
||||
where: {
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
defaultScheduleId: schedule.id,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
return { schedule };
|
||||
@@ -247,7 +271,7 @@ export const availabilityRouter = router({
|
||||
id: user.id,
|
||||
},
|
||||
data: {
|
||||
defaultScheduleId: scheduleToSetAsDefault?.id,
|
||||
defaultScheduleId: scheduleToSetAsDefault?.id || null,
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -450,6 +474,7 @@ const getDefaultScheduleId = async (userId: number, prisma: PrismaClient) => {
|
||||
return user.defaultScheduleId;
|
||||
}
|
||||
|
||||
// If we're returning the default schedule for the first time then we should set it in the user record
|
||||
const defaultSchedule = await prisma.schedule.findFirst({
|
||||
where: {
|
||||
userId,
|
||||
|
||||
@@ -2,6 +2,7 @@ import type { ReactNode } from "react";
|
||||
import React from "react";
|
||||
import type { IconType } from "react-icons";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
import type { SVGComponent } from "@calcom/types/SVGComponent";
|
||||
|
||||
import { Button } from "../../components/button";
|
||||
@@ -14,6 +15,7 @@ export function EmptyScreen({
|
||||
buttonText,
|
||||
buttonOnClick,
|
||||
buttonRaw,
|
||||
border = true,
|
||||
}: {
|
||||
Icon?: SVGComponent | IconType;
|
||||
avatar?: React.ReactElement;
|
||||
@@ -22,12 +24,16 @@ export function EmptyScreen({
|
||||
buttonText?: string;
|
||||
buttonOnClick?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
|
||||
buttonRaw?: ReactNode; // Used incase you want to provide your own button.
|
||||
border?: boolean;
|
||||
}) {
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
data-testid="empty-screen"
|
||||
className="min-h-80 flex w-full flex-col items-center justify-center rounded-md border border-dashed p-7 lg:p-20">
|
||||
className={classNames(
|
||||
"min-h-80 flex w-full flex-col items-center justify-center rounded-md p-7 lg:p-20",
|
||||
border && "border border-dashed"
|
||||
)}>
|
||||
{!avatar ? null : (
|
||||
<div className="flex h-[72px] w-[72px] items-center justify-center rounded-full">{avatar}</div>
|
||||
)}
|
||||
|
||||
Reference in New Issue
Block a user