Add multiselect and batch actions to review items (#10161)

* Implement context menu for batch operations and implement apis

* reduce preview calculations on rerenders

* Add button to mark above items as reviewed

* Use context menu for mark as reviewed

* Cleanup
This commit is contained in:
Nicolas Mowen
2024-02-29 19:53:29 -07:00
committed by GitHub
parent ea9316e770
commit 63d8c3662a
5 changed files with 249 additions and 40 deletions

View File

@@ -1,4 +1,10 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, {
useCallback,
useEffect,
useMemo,
useRef,
useState,
} from "react";
import { useApiHost } from "@/api";
import { isCurrentHour } from "@/utils/dateUtil";
import { ReviewSegment } from "@/types/review";
@@ -16,7 +22,8 @@ import {
ContextMenuSeparator,
ContextMenuTrigger,
} from "../ui/context-menu";
import { LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
import { LuCheckCheck, LuCheckSquare, LuFileUp, LuTrash } from "react-icons/lu";
import { RiCheckboxMultipleLine } from "react-icons/ri";
import axios from "axios";
import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import useImageLoaded from "@/hooks/use-image-loaded";
@@ -25,11 +32,11 @@ import { useSwipeable } from "react-swipeable";
type PreviewPlayerProps = {
review: ReviewSegment;
relevantPreview?: Preview;
autoPlayback?: boolean;
setReviewed?: (reviewId: string) => void;
onClick?: (reviewId: string) => void;
allPreviews?: Preview[];
onTimeUpdate?: (time: number | undefined) => void;
setReviewed: (reviewId: string) => void;
markAboveReviewed: () => void;
onClick: (reviewId: string, ctrl: boolean) => void;
};
type Preview = {
@@ -42,8 +49,9 @@ type Preview = {
export default function PreviewThumbnailPlayer({
review,
relevantPreview,
allPreviews,
setReviewed,
markAboveReviewed,
onClick,
onTimeUpdate,
}: PreviewPlayerProps) {
@@ -57,11 +65,14 @@ export default function PreviewThumbnailPlayer({
// interaction
const handleOnClick = useCallback(() => {
if (onClick && !ignoreClick) {
onClick(review.id);
}
}, [ignoreClick, review, onClick]);
const handleOnClick = useCallback(
(e: React.MouseEvent<HTMLDivElement>) => {
if (!ignoreClick) {
onClick(review.id, e.metaKey);
}
},
[ignoreClick, review, onClick],
);
const swipeHandlers = useSwipeable({
onSwipedLeft: () => (setReviewed ? setReviewed(review.id) : null),
@@ -69,14 +80,24 @@ export default function PreviewThumbnailPlayer({
preventScrollOnSwipe: true,
});
const handleSetReviewed = useCallback(() => {
if (setReviewed) {
setReviewed(review.id);
}
}, [review, setReviewed]);
const handleSetReviewed = useCallback(
() => setReviewed(review.id),
[review, setReviewed],
);
// playback
const relevantPreview = useMemo(
() =>
Object.values(allPreviews || []).find(
(preview) =>
preview.camera == review.camera &&
preview.start < review.start_time &&
preview.end > review.end_time,
),
[allPreviews],
);
const playingBack = useMemo(() => playback, [playback]);
const onPlayback = useCallback(
@@ -186,7 +207,12 @@ export default function PreviewThumbnailPlayer({
)}
</div>
</ContextMenuTrigger>
<PreviewContextItems review={review} setReviewed={handleSetReviewed} />
<PreviewContextItems
review={review}
onSelect={() => onClick(review.id, true)}
setReviewed={handleSetReviewed}
markAboveReviewed={markAboveReviewed}
/>
</ContextMenu>
);
}
@@ -557,11 +583,15 @@ function InProgressPreview({
type PreviewContextItemsProps = {
review: ReviewSegment;
setReviewed?: () => void;
onSelect: () => void;
setReviewed: () => void;
markAboveReviewed: () => void;
};
function PreviewContextItems({
review,
onSelect,
setReviewed,
markAboveReviewed,
}: PreviewContextItemsProps) {
const exportReview = useCallback(() => {
axios.post(
@@ -570,27 +600,46 @@ function PreviewContextItems({
);
}, [review]);
const deleteReview = useCallback(() => {
axios.delete(`reviews/${review.id}`);
}, [review]);
return (
<ContextMenuContent>
{isMobile && (
<ContextMenuItem onSelect={onSelect}>
<div className="w-full flex justify-between items-center">
Select
<RiCheckboxMultipleLine className="ml-4 size-4" />
</div>
</ContextMenuItem>
)}
<ContextMenuItem onSelect={markAboveReviewed}>
<div className="w-full flex justify-between items-center">
Mark Above as Reviewed
<LuCheckCheck className="ml-4 size-4" />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
{!review.has_been_reviewed && (
<ContextMenuItem onSelect={() => (setReviewed ? setReviewed() : null)}>
<div className="w-full flex justify-between items-center">
Mark As Reviewed
<LuCheckSquare className="ml-4 w-4 h-4" />
<LuCheckSquare className="ml-4 size-4" />
</div>
</ContextMenuItem>
)}
<ContextMenuItem onSelect={() => exportReview()}>
<ContextMenuItem onSelect={exportReview}>
<div className="w-full flex justify-between items-center">
Export
<LuFileUp className="ml-4 w-4 h-4" />
<LuFileUp className="ml-4 size-4" />
</div>
</ContextMenuItem>
<ContextMenuSeparator />
<ContextMenuItem>
<ContextMenuItem onSelect={deleteReview}>
<div className="w-full flex justify-between items-center text-danger">
Delete
<LuTrash className="ml-4 w-4 h-4" />
<LuTrash className="ml-4 size-4" />
</div>
</ContextMenuItem>
</ContextMenuContent>