Skip to content

Commit

Permalink
Add confirmation dialog before deleting review items (#12950)
Browse files Browse the repository at this point in the history
  • Loading branch information
hawkeye217 authored Aug 11, 2024
1 parent 9b96211 commit 77bf710
Show file tree
Hide file tree
Showing 5 changed files with 263 additions and 116 deletions.
189 changes: 136 additions & 53 deletions web/src/components/card/ReviewCard.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ import { getIconForLabel } from "@/utils/iconUtil";
import { isDesktop, isIOS, isSafari } from "react-device-detect";
import useSWR from "swr";
import TimeAgo from "../dynamic/TimeAgo";
import { useCallback, useMemo, useState } from "react";
import { useCallback, useMemo, useRef, useState } from "react";
import useImageLoaded from "@/hooks/use-image-loaded";
import ImageLoadingIndicator from "../indicators/ImageLoadingIndicator";
import { FaCompactDisc } from "react-icons/fa";
Expand All @@ -18,9 +18,20 @@ import {
ContextMenuItem,
ContextMenuTrigger,
} from "../ui/context-menu";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import { Drawer, DrawerContent } from "../ui/drawer";
import axios from "axios";
import { toast } from "sonner";
import useKeyboardListener from "@/hooks/use-keyboard-listener";

type ReviewCardProps = {
event: ReviewSegment;
Expand All @@ -46,6 +57,8 @@ export default function ReviewCard({
);

const [optionsOpen, setOptionsOpen] = useState(false);
const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const bypassDialogRef = useRef(false);

const onMarkAsReviewed = useCallback(async () => {
await axios.post(`reviews/viewed`, { ids: [event.id] });
Expand Down Expand Up @@ -92,6 +105,18 @@ export default function ReviewCard({
setOptionsOpen(false);
}, [event]);

useKeyboardListener(["Shift"], (_, modifiers) => {
bypassDialogRef.current = modifiers.shift;
});

const handleDelete = useCallback(() => {
if (bypassDialogRef.current) {
onDelete();
} else {
setDeleteDialogOpen(true);
}
}, [bypassDialogRef, onDelete]);

const content = (
<div
className="relative flex w-full cursor-pointer flex-col gap-1.5"
Expand Down Expand Up @@ -158,71 +183,129 @@ export default function ReviewCard({

if (isDesktop) {
return (
<ContextMenu>
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
</div>
</ContextMenuItem>
{!event.has_been_reviewed && (
<>
<AlertDialog
open={deleteDialogOpen}
onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction className="bg-destructive" onClick={onDelete}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<ContextMenu key={event.id}>
<ContextMenuTrigger asChild>{content}</ContextMenuTrigger>
<ContextMenuContent>
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
onClick={onMarkAsReviewed}
onClick={onExport}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
</div>
</ContextMenuItem>
)}
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
onClick={onDelete}
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">Delete</div>
</div>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
{!event.has_been_reviewed && (
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
</div>
</ContextMenuItem>
)}
<ContextMenuItem>
<div
className="flex w-full cursor-pointer items-center justify-start gap-2 p-2"
onClick={handleDelete}
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
</div>
</div>
</ContextMenuItem>
</ContextMenuContent>
</ContextMenu>
</>
);
}

return (
<Drawer open={optionsOpen} onOpenChange={setOptionsOpen}>
{content}
<DrawerContent>
<div
className="flex w-full items-center justify-start gap-2 p-2"
onClick={onExport}
>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
</div>
{!event.has_been_reviewed && (
<>
<AlertDialog
open={deleteDialogOpen}
onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
this review item?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel onClick={() => setOptionsOpen(false)}>
Cancel
</AlertDialogCancel>
<AlertDialogAction className="bg-destructive" onClick={onDelete}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
<Drawer open={optionsOpen} onOpenChange={setOptionsOpen}>
{content}
<DrawerContent>
<div
className="flex w-full items-center justify-start gap-2 p-2"
onClick={onMarkAsReviewed}
onClick={onExport}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
<FaCompactDisc className="text-secondary-foreground" />
<div className="text-primary">Export</div>
</div>
)}
<div
className="flex w-full items-center justify-start gap-2 p-2"
onClick={onDelete}
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">Delete</div>
</div>
</DrawerContent>
</Drawer>
{!event.has_been_reviewed && (
<div
className="flex w-full items-center justify-start gap-2 p-2"
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
<div className="text-primary">Mark as reviewed</div>
</div>
)}
<div
className="flex w-full items-center justify-start gap-2 p-2"
onClick={handleDelete}
>
<HiTrash className="text-secondary-foreground" />
<div className="text-primary">
{bypassDialogRef.current ? "Delete Now" : "Delete"}
</div>
</div>
</DrawerContent>
</Drawer>
</>
);
}
130 changes: 93 additions & 37 deletions web/src/components/filter/ReviewActionGroup.tsx
Original file line number Diff line number Diff line change
@@ -1,10 +1,21 @@
import { FaCircleCheck } from "react-icons/fa6";
import { useCallback } from "react";
import { useCallback, useState } from "react";
import axios from "axios";
import { Button } from "../ui/button";
import { isDesktop } from "react-device-detect";
import { FaCompactDisc } from "react-icons/fa";
import { HiTrash } from "react-icons/hi";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
import useKeyboardListener from "@/hooks/use-keyboard-listener";

type ReviewActionGroupProps = {
selectedReviews: string[];
Expand Down Expand Up @@ -34,49 +45,94 @@ export default function ReviewActionGroup({
pullLatestData();
}, [selectedReviews, setSelectedReviews, pullLatestData]);

const [deleteDialogOpen, setDeleteDialogOpen] = useState(false);
const [bypassDialog, setBypassDialog] = useState(false);

useKeyboardListener(["Shift"], (_, modifiers) => {
setBypassDialog(modifiers.shift);
});

const handleDelete = useCallback(() => {
if (bypassDialog) {
onDelete();
} else {
setDeleteDialogOpen(true);
}
}, [bypassDialog, onDelete]);

return (
<div className="absolute inset-x-2 inset-y-0 flex items-center justify-between gap-2 bg-background py-2 md:left-auto">
<div className="mx-1 flex items-center justify-center text-sm text-muted-foreground">
<div className="p-1">{`${selectedReviews.length} selected`}</div>
<div className="p-1">{"|"}</div>
<div
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
onClick={onClearSelected}
>
Unselect
<>
<AlertDialog
open={deleteDialogOpen}
onOpenChange={() => setDeleteDialogOpen(!deleteDialogOpen)}
>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirm Delete</AlertDialogTitle>
</AlertDialogHeader>
<AlertDialogDescription>
Are you sure you want to delete all recorded video associated with
the selected review items?
<br />
<br />
Hold the <em>Shift</em> key to bypass this dialog in the future.
</AlertDialogDescription>
<AlertDialogFooter>
<AlertDialogCancel>Cancel</AlertDialogCancel>
<AlertDialogAction className="bg-destructive" onClick={onDelete}>
Delete
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>

<div className="absolute inset-x-2 inset-y-0 flex items-center justify-between gap-2 bg-background py-2 md:left-auto">
<div className="mx-1 flex items-center justify-center text-sm text-muted-foreground">
<div className="p-1">{`${selectedReviews.length} selected`}</div>
<div className="p-1">{"|"}</div>
<div
className="cursor-pointer p-2 text-primary hover:rounded-lg hover:bg-secondary"
onClick={onClearSelected}
>
Unselect
</div>
</div>
</div>
<div className="flex items-center gap-1 md:gap-2">
{selectedReviews.length == 1 && (
<div className="flex items-center gap-1 md:gap-2">
{selectedReviews.length == 1 && (
<Button
className="flex items-center gap-2 p-2"
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
onClearSelected();
}}
>
<FaCompactDisc className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Export</div>}
</Button>
)}
<Button
className="flex items-center gap-2 p-2"
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
onClearSelected();
}}
onClick={onMarkAsReviewed}
>
<FaCompactDisc className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Export</div>}
<FaCircleCheck className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Mark as reviewed</div>}
</Button>
)}
<Button
className="flex items-center gap-2 p-2"
size="sm"
onClick={onMarkAsReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Mark as reviewed</div>}
</Button>
<Button
className="flex items-center gap-2 p-2"
size="sm"
onClick={onDelete}
>
<HiTrash className="text-secondary-foreground" />
{isDesktop && <div className="text-primary">Delete</div>}
</Button>
<Button
className="flex items-center gap-2 p-2"
size="sm"
onClick={handleDelete}
>
<HiTrash className="text-secondary-foreground" />
{isDesktop && (
<div className="text-primary">
{bypassDialog ? "Delete Now" : "Delete"}
</div>
)}
</Button>
</div>
</div>
</div>
</>
);
}
Loading

0 comments on commit 77bf710

Please sign in to comment.