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:
Joe Au-Yeung
2023-03-27 19:53:35 +00:00
committed by GitHub
co-authored by Peer Richelsen
parent 867966dd75
commit a2457ab619
9 changed files with 109 additions and 32 deletions
@@ -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>
+10 -2
View File
@@ -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}
+7 -1
View File
@@ -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: [] };
+1 -1
View File
@@ -63,7 +63,7 @@ import {
FiSettings,
FiSlack,
FiUsers,
FiZap
FiZap,
} from "@calcom/ui/components/icon";
import FreshChatProvider from "../ee/support/lib/freshchat/FreshChatProvider";
+12 -8
View File
@@ -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>
)}