Fix/avoid multiple schedule deletions (#2602)

* Prevents users from deleting the same schedule multiple times due to delay before the schedule disappears. It also applies the same fix to team disband.

Schedule deletion:
![schedule_deletion_new_behaving](https://user-images.githubusercontent.com/42497300/165126805-b3090268-c1a6-418a-b06e-06bd8446da03.gif)

Team disband:
![team_disband_new_behaving](https://user-images.githubusercontent.com/42497300/165127043-7e083e94-e4c9-4e88-90a2-47d31bdd92e6.gif)

Fixes issue [#2569](https://github.com/calcom/cal.com/issues/2569)

Bug fix (non-breaking change which fixes an issue)

**apps/web/components/LightLoader.tsx** → this file was created in order to make a light color loading spinner available. It's necessary when we need to display a loading spinner above dark backgrounds.

**apps/web/components/availability/ScheduleListItem.tsx** → this component was created in order to give a schedule list item its own state.

* Removing a "setTimeout" that was only used for testing purposes

* Adding a code review suggestion to my modifications

* Changing loading style

* Cleanup

* Avoids using unnecessary state

* Revert "Adding a code review suggestion to my modifications"

This reverts commit b5e40062d71157ca1d9384fb1f5c30d50625809d.

* Reverts some changes

* Renames isLoading

Co-authored-by: Alex van Andel <me@alexvanandel.com>
Co-authored-by: Omar López <zomars@me.com>
This commit is contained in:
Arthur Cruz
2022-05-14 15:47:23 -03:00
committed by GitHub
parent 212fd1bc14
commit 2a53614723
8 changed files with 110 additions and 67 deletions

View File

@@ -0,0 +1,75 @@
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
import Link from "next/link";
import { Fragment } from "react";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Availability } from "@calcom/prisma/client";
import { Button } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import { inferQueryOutput } from "@lib/trpc";
export function ScheduleListItem({
schedule,
deleteFunction,
isDeleting = false,
}: {
schedule: inferQueryOutput<"viewer.availability.list">["schedules"][number];
deleteFunction: Function;
isDeleting: boolean;
}) {
const { t, i18n } = useLocale();
return (
<li key={schedule.id}>
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="group flex w-full items-center justify-between hover:bg-neutral-50 sm:px-6">
<Link href={"/availability/" + schedule.id}>
<a className="flex-grow truncate text-sm" title={schedule.name}>
<div>
<span className="truncate font-medium text-neutral-900">{schedule.name}</span>
{schedule.isDefault && (
<span className="ml-2 inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
{t("default")}
</span>
)}
</div>
<p className="mt-1 text-xs text-neutral-500">
{schedule.availability.map((availability: Availability) => (
<Fragment key={availability.id}>
{availabilityAsString(availability, i18n.language)}
<br />
</Fragment>
))}
</p>
</a>
</Link>
</div>
<Dropdown>
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button
disabled={isDeleting}
onClick={() => {
deleteFunction({
scheduleId: schedule.id,
});
}}
type="button"
color="warn"
className="w-full font-normal"
StartIcon={isDeleting ? undefined : TrashIcon}
loading={isDeleting}>
{isDeleting ? t("deleting") : t("delete_schedule")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</li>
);
}

View File

@@ -3,15 +3,15 @@ import { CheckIcon } from "@heroicons/react/solid";
import * as DialogPrimitive from "@radix-ui/react-dialog";
import React, { PropsWithChildren, ReactNode } from "react";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import { Button } from "@calcom/ui/Button";
import { DialogClose, DialogContent } from "@calcom/ui/Dialog";
import { useLocale } from "@lib/hooks/useLocale";
export type ConfirmationDialogContentProps = {
confirmBtn?: ReactNode;
confirmBtnText?: string;
cancelBtnText?: string;
isLoading?: boolean;
onConfirm?: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void;
title: string;
variety?: "danger" | "warning" | "success";
@@ -25,6 +25,7 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
confirmBtn = null,
confirmBtnText = t("confirm"),
cancelBtnText = t("cancel"),
isLoading = false,
onConfirm,
children,
} = props;
@@ -59,10 +60,14 @@ export default function ConfirmationDialogContent(props: PropsWithChildren<Confi
</div>
</div>
<div className="mt-5 flex flex-row-reverse gap-x-2 sm:mt-8">
<DialogClose onClick={onConfirm} asChild>
{confirmBtn || <Button color="primary">{confirmBtnText}</Button>}
<DialogClose disabled={isLoading} onClick={onConfirm} asChild>
{confirmBtn || (
<Button color="primary" loading={isLoading}>
{isLoading ? t("loading") : confirmBtnText}
</Button>
)}
</DialogClose>
<DialogClose asChild>
<DialogClose disabled={isLoading} asChild>
<Button color="secondary">{cancelBtnText}</Button>
</DialogClose>
</div>

View File

@@ -1,6 +1,6 @@
import showToast from "@calcom/lib/notification";
import { trpc, inferQueryOutput } from "@lib/trpc";
import { inferQueryOutput, trpc } from "@lib/trpc";
import TeamListItem from "./TeamListItem";
@@ -39,7 +39,9 @@ export default function TeamList(props: Props) {
<TeamListItem
key={team?.id as number}
team={team}
onActionSelect={(action: string) => selectAction(action, team?.id as number)}></TeamListItem>
onActionSelect={(action: string) => selectAction(action, team?.id as number)}
isLoading={deleteTeamMutation.isLoading}
/>
))}
</ul>
</div>

View File

@@ -1,10 +1,10 @@
import { LogoutIcon } from "@heroicons/react/outline";
import {
ExternalLinkIcon,
TrashIcon,
LinkIcon,
DotsHorizontalIcon,
ExternalLinkIcon,
LinkIcon,
PencilIcon,
TrashIcon,
} from "@heroicons/react/solid";
import { MembershipRole } from "@prisma/client";
import Link from "next/link";
@@ -16,14 +16,14 @@ import { Dialog, DialogTrigger } from "@calcom/ui/Dialog";
import Dropdown, {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@calcom/ui/Dropdown";
import { Tooltip } from "@calcom/ui/Tooltip";
import classNames from "@lib/classNames";
import { getPlaceholderAvatar } from "@lib/getPlaceholderAvatar";
import { trpc, inferQueryOutput } from "@lib/trpc";
import { inferQueryOutput, trpc } from "@lib/trpc";
import ConfirmationDialogContent from "@components/dialog/ConfirmationDialogContent";
import Avatar from "@components/ui/Avatar";
@@ -34,6 +34,7 @@ interface Props {
team: inferQueryOutput<"viewer.teams.list">[number];
key: number;
onActionSelect: (text: string) => void;
isLoading?: boolean;
}
export default function TeamListItem(props: Props) {
@@ -175,7 +176,10 @@ export default function TeamListItem(props: Props) {
variety="danger"
title={t("disband_team")}
confirmBtnText={t("confirm_disband_team")}
onConfirm={() => props.onActionSelect("disband")}>
isLoading={props.isLoading}
onConfirm={() => {
props.onActionSelect("disband");
}}>
{t("disband_team_confirmation_message")}
</ConfirmationDialogContent>
</Dialog>

View File

@@ -1,13 +1,7 @@
import { ClockIcon } from "@heroicons/react/outline";
import { DotsHorizontalIcon, TrashIcon } from "@heroicons/react/solid";
import { Availability } from "@prisma/client";
import Link from "next/link";
import { availabilityAsString } from "@calcom/lib/availability";
import { useLocale } from "@calcom/lib/hooks/useLocale";
import showToast from "@calcom/lib/notification";
import { Button } from "@calcom/ui";
import Dropdown, { DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@calcom/ui/Dropdown";
import { withQuery } from "@lib/QueryCell";
import { HttpError } from "@lib/core/http/error";
@@ -16,10 +10,11 @@ import { inferQueryOutput, trpc } from "@lib/trpc";
import EmptyScreen from "@components/EmptyScreen";
import Shell from "@components/Shell";
import { NewScheduleButton } from "@components/availability/NewScheduleButton";
import { ScheduleListItem } from "@components/availability/ScheduleListItem";
import SkeletonLoader from "@components/availability/SkeletonLoader";
export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availability.list">) {
const { t, i18n } = useLocale();
const { t } = useLocale();
const utils = trpc.useContext();
const deleteMutation = trpc.useMutation("viewer.availability.schedule.delete", {
onSuccess: async () => {
@@ -45,53 +40,12 @@ export function AvailabilityList({ schedules }: inferQueryOutput<"viewer.availab
<div className="-mx-4 mb-16 overflow-hidden rounded-sm border border-gray-200 bg-white sm:mx-0">
<ul className="divide-y divide-neutral-200" data-testid="schedules">
{schedules.map((schedule) => (
<li key={schedule.id}>
<div className="flex items-center justify-between py-5 hover:bg-neutral-50 ltr:pl-4 rtl:pr-4 sm:ltr:pl-0 sm:rtl:pr-0">
<div className="group flex w-full items-center justify-between hover:bg-neutral-50 sm:px-6">
<Link href={"/availability/" + schedule.id}>
<a className="flex-grow truncate text-sm" title={schedule.name}>
<div>
<span className="truncate font-medium text-neutral-900">{schedule.name}</span>
{schedule.isDefault && (
<span className="ml-2 inline items-center rounded-sm bg-yellow-100 px-1.5 py-0.5 text-xs font-medium text-yellow-800">
{t("default")}
</span>
)}
</div>
<p className="mt-1 text-xs text-neutral-500">
{schedule.availability.map((availability: Availability) => (
<>
{availabilityAsString(availability, i18n.language)}
<br />
</>
))}
</p>
</a>
</Link>
</div>
<Dropdown>
<DropdownMenuTrigger className="group mr-5 h-10 w-10 border border-transparent p-0 text-neutral-500 hover:border-gray-200">
<DotsHorizontalIcon className="h-5 w-5 group-hover:text-gray-800" />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem>
<Button
onClick={() =>
deleteMutation.mutate({
scheduleId: schedule.id,
})
}
type="button"
color="warn"
className="w-full font-normal"
StartIcon={TrashIcon}>
{t("delete_schedule")}
</Button>
</DropdownMenuItem>
</DropdownMenuContent>
</Dropdown>
</div>
</li>
<ScheduleListItem
key={schedule.id}
schedule={schedule}
deleteFunction={deleteMutation.mutate}
isDeleting={deleteMutation.isLoading}
/>
))}
</ul>
</div>

View File

@@ -348,6 +348,7 @@
"receive_cal_event_meeting_data": "Receive Cal meeting data at a specified URL, in real-time, when this event is scheduled or cancelled.",
"responsive_fullscreen_iframe": "Responsive full screen iframe",
"loading": "Loading...",
"deleting": "Deleting...",
"standard_iframe": "Standard iframe",
"iframe_embed": "iframe Embed",
"embed_calcom": "The easiest way to embed Cal.com on your website.",

View File

@@ -292,6 +292,7 @@
"receive_cal_meeting_data": "Receba dados da reunião Cal em uma, URL especificada, em tempo real, quando um evento for agendado ou cancelado.",
"responsive_fullscreen_iframe": "Iframe responsivo de tela inteira",
"loading": "Carregando...",
"deleting": "Apagando...",
"standard_iframe": "Iframe padrão",
"iframe_embed": "Incorporar iframe",
"embed_calcom": "A maneira mais fácil de incorporar o Cal.com no seu site.",

View File

@@ -329,6 +329,7 @@
"receive_cal_event_meeting_data": "Receba dados da reunião Cal num URL especificado, em tempo real, assim que o evento for agendado ou cancelado.",
"responsive_fullscreen_iframe": "Iframe responsivo de ecrã inteiro",
"loading": "A carregar...",
"deleting": "A remover...",
"standard_iframe": "Iframe padrão",
"iframe_embed": "Incorporar iframe",
"embed_calcom": "A maneira mais fácil de incorporar o Cal.com no seu site.",