Add ability to mark review items as unreviewed (#20446)

* new body param

* use new body param in endpoint

* explicitly use new param in frontend endpoint

* use reviewsegment as type instead of list of strings

* add toggle function to mark as unreviewed when all selected are reviewed

* i18n

* fix tests
This commit is contained in:
Josh Hawkins
2025-10-12 08:10:56 -05:00
committed by GitHub
parent a2ad77c36e
commit 6d5098a0c2
7 changed files with 60 additions and 28 deletions

View File

@@ -1,10 +1,11 @@
import { FaCircleCheck } from "react-icons/fa6";
import { FaCircleCheck, FaCircleXmark } from "react-icons/fa6";
import { useCallback, useState } from "react";
import axios from "axios";
import { Button, buttonVariants } from "../ui/button";
import { isDesktop } from "react-device-detect";
import { FaCompactDisc } from "react-icons/fa";
import { HiTrash } from "react-icons/hi";
import { ReviewSegment } from "@/types/review";
import {
AlertDialog,
AlertDialogAction,
@@ -20,8 +21,8 @@ import { Trans, useTranslation } from "react-i18next";
import { toast } from "sonner";
type ReviewActionGroupProps = {
selectedReviews: string[];
setSelectedReviews: (ids: string[]) => void;
selectedReviews: ReviewSegment[];
setSelectedReviews: (reviews: ReviewSegment[]) => void;
onExport: (id: string) => void;
pullLatestData: () => void;
};
@@ -36,15 +37,24 @@ export default function ReviewActionGroup({
setSelectedReviews([]);
}, [setSelectedReviews]);
const onMarkAsReviewed = useCallback(async () => {
await axios.post(`reviews/viewed`, { ids: selectedReviews });
const allReviewed = selectedReviews.every(
(review) => review.has_been_reviewed,
);
const onToggleReviewed = useCallback(async () => {
const ids = selectedReviews.map((review) => review.id);
await axios.post(`reviews/viewed`, {
ids,
reviewed: !allReviewed,
});
setSelectedReviews([]);
pullLatestData();
}, [selectedReviews, setSelectedReviews, pullLatestData]);
}, [selectedReviews, setSelectedReviews, pullLatestData, allReviewed]);
const onDelete = useCallback(() => {
const ids = selectedReviews.map((review) => review.id);
axios
.post(`reviews/delete`, { ids: selectedReviews })
.post(`reviews/delete`, { ids })
.then((resp) => {
if (resp.status === 200) {
toast.success(t("recording.confirmDelete.toast.success"), {
@@ -140,7 +150,7 @@ export default function ReviewActionGroup({
aria-label={t("recording.button.export")}
size="sm"
onClick={() => {
onExport(selectedReviews[0]);
onExport(selectedReviews[0].id);
onClearSelected();
}}
>
@@ -154,14 +164,24 @@ export default function ReviewActionGroup({
)}
<Button
className="flex items-center gap-2 p-2"
aria-label={t("recording.button.markAsReviewed")}
aria-label={
allReviewed
? t("recording.button.markAsUnreviewed")
: t("recording.button.markAsReviewed")
}
size="sm"
onClick={onMarkAsReviewed}
onClick={onToggleReviewed}
>
<FaCircleCheck className="text-secondary-foreground" />
{allReviewed ? (
<FaCircleXmark className="text-secondary-foreground" />
) : (
<FaCircleCheck className="text-secondary-foreground" />
)}
{isDesktop && (
<div className="text-primary">
{t("recording.button.markAsReviewed")}
{allReviewed
? t("recording.button.markAsUnreviewed")
: t("recording.button.markAsReviewed")}
</div>
)}
</Button>

View File

@@ -356,6 +356,7 @@ export default function Events() {
if (itemsToMarkReviewed.length > 0) {
await axios.post(`reviews/viewed`, {
ids: itemsToMarkReviewed,
reviewed: true,
});
reloadData();
}
@@ -365,7 +366,10 @@ export default function Events() {
const markItemAsReviewed = useCallback(
async (review: ReviewSegment) => {
const resp = await axios.post(`reviews/viewed`, { ids: [review.id] });
const resp = await axios.post(`reviews/viewed`, {
ids: [review.id],
reviewed: true,
});
if (resp.status == 200) {
updateSegments(

View File

@@ -135,11 +135,11 @@ export default function EventView({
// review interaction
const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
const [selectedReviews, setSelectedReviews] = useState<ReviewSegment[]>([]);
const onSelectReview = useCallback(
(review: ReviewSegment, ctrl: boolean) => {
if (selectedReviews.length > 0 || ctrl) {
const index = selectedReviews.indexOf(review.id);
const index = selectedReviews.findIndex((r) => r.id === review.id);
if (index != -1) {
if (selectedReviews.length == 1) {
@@ -153,7 +153,7 @@ export default function EventView({
}
} else {
const copy = [...selectedReviews];
copy.push(review.id);
copy.push(review);
setSelectedReviews(copy);
}
} else {
@@ -175,7 +175,7 @@ export default function EventView({
}
if (selectedReviews.length < currentReviewItems.length) {
setSelectedReviews(currentReviewItems.map((seg) => seg.id));
setSelectedReviews(currentReviewItems);
} else {
setSelectedReviews([]);
}
@@ -429,7 +429,7 @@ type DetectionReviewProps = {
currentItems: ReviewSegment[] | null;
itemsToReview?: number;
relevantPreviews?: Preview[];
selectedReviews: string[];
selectedReviews: ReviewSegment[];
severity: ReviewSeverity;
filter?: ReviewFilter;
timeRange: { before: number; after: number };
@@ -439,7 +439,7 @@ type DetectionReviewProps = {
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
onSelectAllReviews: () => void;
setSelectedReviews: (reviewIds: string[]) => void;
setSelectedReviews: (reviews: ReviewSegment[]) => void;
pullLatestData: () => void;
};
function DetectionReview({
@@ -667,7 +667,7 @@ function DetectionReview({
case "r":
if (selectedReviews.length > 0 && !modifiers.repeat) {
currentItems?.forEach((item) => {
if (selectedReviews.includes(item.id)) {
if (selectedReviews.some((r) => r.id === item.id)) {
item.has_been_reviewed = true;
markItemAsReviewed(item);
}
@@ -723,7 +723,7 @@ function DetectionReview({
>
{!loading && currentItems
? currentItems.map((value) => {
const selected = selectedReviews.includes(value.id);
const selected = selectedReviews.some((r) => r.id === value.id);
return (
<div