mirror of
https://github.com/blakeblackshear/frigate.git
synced 2025-12-06 05:24:11 +03:00
Add ability to mark review items as unreviewed (#20446)
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
Some checks are pending
CI / AMD64 Build (push) Waiting to run
CI / ARM Build (push) Waiting to run
CI / Jetson Jetpack 6 (push) Waiting to run
CI / AMD64 Extra Build (push) Blocked by required conditions
CI / ARM Extra Build (push) Blocked by required conditions
CI / Synaptics Build (push) Blocked by required conditions
CI / Assemble and push default build (push) Blocked by required conditions
* 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:
parent
a2ad77c36e
commit
6d5098a0c2
@ -4,3 +4,5 @@ from pydantic import BaseModel, conlist, constr
|
|||||||
class ReviewModifyMultipleBody(BaseModel):
|
class ReviewModifyMultipleBody(BaseModel):
|
||||||
# List of string with at least one element and each element with at least one char
|
# List of string with at least one element and each element with at least one char
|
||||||
ids: conlist(constr(min_length=1), min_length=1)
|
ids: conlist(constr(min_length=1), min_length=1)
|
||||||
|
# Whether to mark items as reviewed (True) or unreviewed (False)
|
||||||
|
reviewed: bool = True
|
||||||
|
|||||||
@ -435,22 +435,27 @@ async def set_multiple_reviewed(
|
|||||||
UserReviewStatus.user_id == user_id,
|
UserReviewStatus.user_id == user_id,
|
||||||
UserReviewStatus.review_segment == review_id,
|
UserReviewStatus.review_segment == review_id,
|
||||||
)
|
)
|
||||||
# If it exists and isn’t reviewed, update it
|
# Update based on the reviewed parameter
|
||||||
if not review_status.has_been_reviewed:
|
if review_status.has_been_reviewed != body.reviewed:
|
||||||
review_status.has_been_reviewed = True
|
review_status.has_been_reviewed = body.reviewed
|
||||||
review_status.save()
|
review_status.save()
|
||||||
except DoesNotExist:
|
except DoesNotExist:
|
||||||
try:
|
try:
|
||||||
UserReviewStatus.create(
|
UserReviewStatus.create(
|
||||||
user_id=user_id,
|
user_id=user_id,
|
||||||
review_segment=ReviewSegment.get(id=review_id),
|
review_segment=ReviewSegment.get(id=review_id),
|
||||||
has_been_reviewed=True,
|
has_been_reviewed=body.reviewed,
|
||||||
)
|
)
|
||||||
except (DoesNotExist, IntegrityError):
|
except (DoesNotExist, IntegrityError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
return JSONResponse(
|
return JSONResponse(
|
||||||
content=({"success": True, "message": "Reviewed multiple items"}),
|
content=(
|
||||||
|
{
|
||||||
|
"success": True,
|
||||||
|
"message": f"Marked multiple items as {'reviewed' if body.reviewed else 'unreviewed'}",
|
||||||
|
}
|
||||||
|
),
|
||||||
status_code=200,
|
status_code=200,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@ -416,7 +416,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response = response.json()
|
response = response.json()
|
||||||
assert response["success"] == True
|
assert response["success"] == True
|
||||||
assert response["message"] == "Reviewed multiple items"
|
assert response["message"] == "Marked multiple items as reviewed"
|
||||||
# Verify that in DB the review segment was not changed
|
# Verify that in DB the review segment was not changed
|
||||||
with self.assertRaises(DoesNotExist):
|
with self.assertRaises(DoesNotExist):
|
||||||
UserReviewStatus.get(
|
UserReviewStatus.get(
|
||||||
@ -433,7 +433,7 @@ class TestHttpReview(BaseTestHttp):
|
|||||||
assert response.status_code == 200
|
assert response.status_code == 200
|
||||||
response_json = response.json()
|
response_json = response.json()
|
||||||
assert response_json["success"] == True
|
assert response_json["success"] == True
|
||||||
assert response_json["message"] == "Reviewed multiple items"
|
assert response_json["message"] == "Marked multiple items as reviewed"
|
||||||
# Verify UserReviewStatus was created
|
# Verify UserReviewStatus was created
|
||||||
user_review = UserReviewStatus.get(
|
user_review = UserReviewStatus.get(
|
||||||
UserReviewStatus.user_id == self.user_id,
|
UserReviewStatus.user_id == self.user_id,
|
||||||
|
|||||||
@ -106,6 +106,7 @@
|
|||||||
"button": {
|
"button": {
|
||||||
"export": "Export",
|
"export": "Export",
|
||||||
"markAsReviewed": "Mark as reviewed",
|
"markAsReviewed": "Mark as reviewed",
|
||||||
|
"markAsUnreviewed": "Mark as unreviewed",
|
||||||
"deleteNow": "Delete Now"
|
"deleteNow": "Delete Now"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|||||||
@ -1,10 +1,11 @@
|
|||||||
import { FaCircleCheck } from "react-icons/fa6";
|
import { FaCircleCheck, FaCircleXmark } from "react-icons/fa6";
|
||||||
import { useCallback, useState } from "react";
|
import { useCallback, useState } from "react";
|
||||||
import axios from "axios";
|
import axios from "axios";
|
||||||
import { Button, buttonVariants } from "../ui/button";
|
import { Button, buttonVariants } from "../ui/button";
|
||||||
import { isDesktop } from "react-device-detect";
|
import { isDesktop } from "react-device-detect";
|
||||||
import { FaCompactDisc } from "react-icons/fa";
|
import { FaCompactDisc } from "react-icons/fa";
|
||||||
import { HiTrash } from "react-icons/hi";
|
import { HiTrash } from "react-icons/hi";
|
||||||
|
import { ReviewSegment } from "@/types/review";
|
||||||
import {
|
import {
|
||||||
AlertDialog,
|
AlertDialog,
|
||||||
AlertDialogAction,
|
AlertDialogAction,
|
||||||
@ -20,8 +21,8 @@ import { Trans, useTranslation } from "react-i18next";
|
|||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
type ReviewActionGroupProps = {
|
type ReviewActionGroupProps = {
|
||||||
selectedReviews: string[];
|
selectedReviews: ReviewSegment[];
|
||||||
setSelectedReviews: (ids: string[]) => void;
|
setSelectedReviews: (reviews: ReviewSegment[]) => void;
|
||||||
onExport: (id: string) => void;
|
onExport: (id: string) => void;
|
||||||
pullLatestData: () => void;
|
pullLatestData: () => void;
|
||||||
};
|
};
|
||||||
@ -36,15 +37,24 @@ export default function ReviewActionGroup({
|
|||||||
setSelectedReviews([]);
|
setSelectedReviews([]);
|
||||||
}, [setSelectedReviews]);
|
}, [setSelectedReviews]);
|
||||||
|
|
||||||
const onMarkAsReviewed = useCallback(async () => {
|
const allReviewed = selectedReviews.every(
|
||||||
await axios.post(`reviews/viewed`, { ids: selectedReviews });
|
(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([]);
|
setSelectedReviews([]);
|
||||||
pullLatestData();
|
pullLatestData();
|
||||||
}, [selectedReviews, setSelectedReviews, pullLatestData]);
|
}, [selectedReviews, setSelectedReviews, pullLatestData, allReviewed]);
|
||||||
|
|
||||||
const onDelete = useCallback(() => {
|
const onDelete = useCallback(() => {
|
||||||
|
const ids = selectedReviews.map((review) => review.id);
|
||||||
axios
|
axios
|
||||||
.post(`reviews/delete`, { ids: selectedReviews })
|
.post(`reviews/delete`, { ids })
|
||||||
.then((resp) => {
|
.then((resp) => {
|
||||||
if (resp.status === 200) {
|
if (resp.status === 200) {
|
||||||
toast.success(t("recording.confirmDelete.toast.success"), {
|
toast.success(t("recording.confirmDelete.toast.success"), {
|
||||||
@ -140,7 +150,7 @@ export default function ReviewActionGroup({
|
|||||||
aria-label={t("recording.button.export")}
|
aria-label={t("recording.button.export")}
|
||||||
size="sm"
|
size="sm"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
onExport(selectedReviews[0]);
|
onExport(selectedReviews[0].id);
|
||||||
onClearSelected();
|
onClearSelected();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@ -154,14 +164,24 @@ export default function ReviewActionGroup({
|
|||||||
)}
|
)}
|
||||||
<Button
|
<Button
|
||||||
className="flex items-center gap-2 p-2"
|
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"
|
size="sm"
|
||||||
onClick={onMarkAsReviewed}
|
onClick={onToggleReviewed}
|
||||||
>
|
>
|
||||||
<FaCircleCheck className="text-secondary-foreground" />
|
{allReviewed ? (
|
||||||
|
<FaCircleXmark className="text-secondary-foreground" />
|
||||||
|
) : (
|
||||||
|
<FaCircleCheck className="text-secondary-foreground" />
|
||||||
|
)}
|
||||||
{isDesktop && (
|
{isDesktop && (
|
||||||
<div className="text-primary">
|
<div className="text-primary">
|
||||||
{t("recording.button.markAsReviewed")}
|
{allReviewed
|
||||||
|
? t("recording.button.markAsUnreviewed")
|
||||||
|
: t("recording.button.markAsReviewed")}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -356,6 +356,7 @@ export default function Events() {
|
|||||||
if (itemsToMarkReviewed.length > 0) {
|
if (itemsToMarkReviewed.length > 0) {
|
||||||
await axios.post(`reviews/viewed`, {
|
await axios.post(`reviews/viewed`, {
|
||||||
ids: itemsToMarkReviewed,
|
ids: itemsToMarkReviewed,
|
||||||
|
reviewed: true,
|
||||||
});
|
});
|
||||||
reloadData();
|
reloadData();
|
||||||
}
|
}
|
||||||
@ -365,7 +366,10 @@ export default function Events() {
|
|||||||
|
|
||||||
const markItemAsReviewed = useCallback(
|
const markItemAsReviewed = useCallback(
|
||||||
async (review: ReviewSegment) => {
|
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) {
|
if (resp.status == 200) {
|
||||||
updateSegments(
|
updateSegments(
|
||||||
|
|||||||
@ -135,11 +135,11 @@ export default function EventView({
|
|||||||
|
|
||||||
// review interaction
|
// review interaction
|
||||||
|
|
||||||
const [selectedReviews, setSelectedReviews] = useState<string[]>([]);
|
const [selectedReviews, setSelectedReviews] = useState<ReviewSegment[]>([]);
|
||||||
const onSelectReview = useCallback(
|
const onSelectReview = useCallback(
|
||||||
(review: ReviewSegment, ctrl: boolean) => {
|
(review: ReviewSegment, ctrl: boolean) => {
|
||||||
if (selectedReviews.length > 0 || ctrl) {
|
if (selectedReviews.length > 0 || ctrl) {
|
||||||
const index = selectedReviews.indexOf(review.id);
|
const index = selectedReviews.findIndex((r) => r.id === review.id);
|
||||||
|
|
||||||
if (index != -1) {
|
if (index != -1) {
|
||||||
if (selectedReviews.length == 1) {
|
if (selectedReviews.length == 1) {
|
||||||
@ -153,7 +153,7 @@ export default function EventView({
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
const copy = [...selectedReviews];
|
const copy = [...selectedReviews];
|
||||||
copy.push(review.id);
|
copy.push(review);
|
||||||
setSelectedReviews(copy);
|
setSelectedReviews(copy);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@ -175,7 +175,7 @@ export default function EventView({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (selectedReviews.length < currentReviewItems.length) {
|
if (selectedReviews.length < currentReviewItems.length) {
|
||||||
setSelectedReviews(currentReviewItems.map((seg) => seg.id));
|
setSelectedReviews(currentReviewItems);
|
||||||
} else {
|
} else {
|
||||||
setSelectedReviews([]);
|
setSelectedReviews([]);
|
||||||
}
|
}
|
||||||
@ -429,7 +429,7 @@ type DetectionReviewProps = {
|
|||||||
currentItems: ReviewSegment[] | null;
|
currentItems: ReviewSegment[] | null;
|
||||||
itemsToReview?: number;
|
itemsToReview?: number;
|
||||||
relevantPreviews?: Preview[];
|
relevantPreviews?: Preview[];
|
||||||
selectedReviews: string[];
|
selectedReviews: ReviewSegment[];
|
||||||
severity: ReviewSeverity;
|
severity: ReviewSeverity;
|
||||||
filter?: ReviewFilter;
|
filter?: ReviewFilter;
|
||||||
timeRange: { before: number; after: number };
|
timeRange: { before: number; after: number };
|
||||||
@ -439,7 +439,7 @@ type DetectionReviewProps = {
|
|||||||
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
markAllItemsAsReviewed: (currentItems: ReviewSegment[]) => void;
|
||||||
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
|
onSelectReview: (review: ReviewSegment, ctrl: boolean) => void;
|
||||||
onSelectAllReviews: () => void;
|
onSelectAllReviews: () => void;
|
||||||
setSelectedReviews: (reviewIds: string[]) => void;
|
setSelectedReviews: (reviews: ReviewSegment[]) => void;
|
||||||
pullLatestData: () => void;
|
pullLatestData: () => void;
|
||||||
};
|
};
|
||||||
function DetectionReview({
|
function DetectionReview({
|
||||||
@ -667,7 +667,7 @@ function DetectionReview({
|
|||||||
case "r":
|
case "r":
|
||||||
if (selectedReviews.length > 0 && !modifiers.repeat) {
|
if (selectedReviews.length > 0 && !modifiers.repeat) {
|
||||||
currentItems?.forEach((item) => {
|
currentItems?.forEach((item) => {
|
||||||
if (selectedReviews.includes(item.id)) {
|
if (selectedReviews.some((r) => r.id === item.id)) {
|
||||||
item.has_been_reviewed = true;
|
item.has_been_reviewed = true;
|
||||||
markItemAsReviewed(item);
|
markItemAsReviewed(item);
|
||||||
}
|
}
|
||||||
@ -723,7 +723,7 @@ function DetectionReview({
|
|||||||
>
|
>
|
||||||
{!loading && currentItems
|
{!loading && currentItems
|
||||||
? currentItems.map((value) => {
|
? currentItems.map((value) => {
|
||||||
const selected = selectedReviews.includes(value.id);
|
const selected = selectedReviews.some((r) => r.id === value.id);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user