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:  Team disband:  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:
75
apps/web/components/availability/ScheduleListItem.tsx
Normal file
75
apps/web/components/availability/ScheduleListItem.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
@@ -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.",
|
||||
|
||||
Reference in New Issue
Block a user