From ae8be19b5ba103824a5185548feb3fb824a92d16 Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Sat, 11 Apr 2026 22:09:42 -0500 Subject: [PATCH] tweaks - hide cases when filtering cameras that have no exports from those cameras - remove description from case card - use textarea instead of input for case description in add new case dialog --- web/public/locales/en/views/exports.json | 5 +- web/src/components/card/ExportCard.tsx | 14 +- .../overlay/dialog/OptionAndInputDialog.tsx | 4 +- web/src/pages/Exports.tsx | 125 +++++++++++++----- 4 files changed, 105 insertions(+), 43 deletions(-) diff --git a/web/public/locales/en/views/exports.json b/web/public/locales/en/views/exports.json index e0ace7d24..6745d3578 100644 --- a/web/public/locales/en/views/exports.json +++ b/web/public/locales/en/views/exports.json @@ -39,7 +39,10 @@ }, "deleteCase": { "label": "Delete Case", - "desc": "Are you sure you want to delete {{caseName}}? Exports will remain available as uncategorized exports." + "desc": "Are you sure you want to delete {{caseName}}?", + "descKeepExports": "Exports will remain available as uncategorized exports.", + "descDeleteExports": "All exports in this case will be permanently deleted.", + "deleteExports": "Also delete exports" }, "caseDialog": { "title": "Add to case", diff --git a/web/src/components/card/ExportCard.tsx b/web/src/components/card/ExportCard.tsx index c1689139b..9411dd7a8 100644 --- a/web/src/components/card/ExportCard.tsx +++ b/web/src/components/card/ExportCard.tsx @@ -87,11 +87,11 @@ export function CaseCard({
{exportCase.name}
-
- {exports.length === 0 - ? t("caseCard.emptyCase") - : exportCase.description} -
+ {exports.length === 0 && ( +
+ {t("caseCard.emptyCase")} +
+ )} ); @@ -333,8 +333,8 @@ export function ExportCard({ )} -
-
+
+
{exportedRecording.name.replaceAll("_", " ")}
diff --git a/web/src/components/overlay/dialog/OptionAndInputDialog.tsx b/web/src/components/overlay/dialog/OptionAndInputDialog.tsx index cb6b23907..19cffc806 100644 --- a/web/src/components/overlay/dialog/OptionAndInputDialog.tsx +++ b/web/src/components/overlay/dialog/OptionAndInputDialog.tsx @@ -8,6 +8,7 @@ import { DialogTitle, } from "@/components/ui/dialog"; import { Input } from "@/components/ui/input"; +import { Textarea } from "@/components/ui/textarea"; import { Select, SelectContent, @@ -133,9 +134,10 @@ export default function OptionAndInputDialog({ - setDescriptionValue(e.target.value)} + rows={2} />
diff --git a/web/src/pages/Exports.tsx b/web/src/pages/Exports.tsx index 43e767d51..9a7ce9077 100644 --- a/web/src/pages/Exports.tsx +++ b/web/src/pages/Exports.tsx @@ -23,6 +23,8 @@ import { import Heading from "@/components/ui/heading"; import { Input } from "@/components/ui/input"; import { Textarea } from "@/components/ui/textarea"; +import { Label } from "@/components/ui/label"; +import { Switch } from "@/components/ui/switch"; import { Toaster } from "@/components/ui/sonner"; import useKeyboardListener from "@/hooks/use-keyboard-listener"; import { useSearchEffect } from "@/hooks/use-overlay-state"; @@ -83,7 +85,7 @@ function Exports() { const { data: cases, mutate: updateCases } = useSWR("cases"); const { data: activeExportJobs } = useSWR("jobs/export", { - refreshInterval: 2000, + refreshInterval: (latestJobs) => ((latestJobs ?? []).length > 0 ? 2000 : 0), }); // Keep polling exports while there are queued/running jobs OR while any // existing export is still marked in_progress. Without the second clause, @@ -153,8 +155,21 @@ function Exports() { }, [rawExports]); const filteredCases = useMemo(() => { - return cases || []; - }, [cases]); + if (!cases) return []; + + const hasCameraFilter = + exportFilter?.cameras && exportFilter.cameras.length > 0; + + if (!hasCameraFilter) return cases; + + // When a camera filter is active, hide cases that have zero exports + // and zero active jobs matching the filter — they're just noise. + return cases.filter( + (c) => + (exportsByCase[c.id]?.length ?? 0) > 0 || + (activeJobsByCase[c.id]?.length ?? 0) > 0, + ); + }, [activeJobsByCase, cases, exportFilter?.cameras, exportsByCase]); const exports = useMemo( () => exportsByCase["none"] || [], @@ -217,6 +232,7 @@ function Exports() { { mode: "create" | "edit"; exportCase?: ExportCase } | undefined >(); const [caseToDelete, setCaseToDelete] = useState(); + const [deleteExportsWithCase, setDeleteExportsWithCase] = useState(false); const [caseForAddExport, setCaseForAddExport] = useState< ExportCase | undefined >(); @@ -333,11 +349,14 @@ function Exports() { } try { - await axios.delete(`cases/${caseToDelete.id}`); + await axios.delete(`cases/${caseToDelete.id}`, { + params: deleteExportsWithCase ? { delete_exports: true } : undefined, + }); if (selectedCaseId === caseToDelete.id) { setSelectedCaseId(undefined); } setCaseToDelete(undefined); + setDeleteExportsWithCase(false); mutate(); } catch (error) { const apiError = error as { @@ -351,7 +370,7 @@ function Exports() { position: "top-center", }); } - }, [caseToDelete, mutate, selectedCaseId, t]); + }, [caseToDelete, deleteExportsWithCase, mutate, selectedCaseId, t]); const handleAssignExportToCase = useCallback( async (exportId: string, caseId: string) => { @@ -457,7 +476,12 @@ function Exports() { setCaseToDelete(undefined)} + onOpenChange={(open) => { + if (!open) { + setCaseToDelete(undefined); + setDeleteExportsWithCase(false); + } + }} > @@ -465,9 +489,25 @@ function Exports() { {t("deleteCase.desc", { caseName: caseToDelete?.name, - })} + })}{" "} + {deleteExportsWithCase + ? t("deleteCase.descDeleteExports") + : t("deleteCase.descKeepExports")} +
+ + +
{t("button.cancel", { ns: "common" })} @@ -526,7 +566,7 @@ function Exports() {
@@ -574,32 +614,49 @@ function Exports() { )} {selectedCase && (
- - - + +
+ + + +
)}