add ability to use regexes in the plate search box and add select all/clear all links to quickly select all filtered plates
This commit is contained in:
Josh Hawkins 2025-08-26 07:50:24 -05:00
parent d85f58f3be
commit f2b87e7a07
2 changed files with 78 additions and 6 deletions

View File

@ -127,6 +127,8 @@
"loading": "Loading recognized license plates…",
"placeholder": "Type to search license plates…",
"noLicensePlatesFound": "No license plates found.",
"selectPlatesFromList": "Select one or more plates from the list."
"selectPlatesFromList": "Select one or more plates from the list.",
"selectAll": "Select all",
"clearAll": "Clear all"
}
}

View File

@ -41,7 +41,7 @@ import {
CommandItem,
CommandList,
} from "@/components/ui/command";
import { LuCheck } from "react-icons/lu";
import { LuCheck, LuSquareCheck, LuX } from "react-icons/lu";
import ActivityIndicator from "@/components/indicators/activity-indicator";
type SearchFilterDialogProps = {
@ -923,13 +923,19 @@ export function RecognizedLicensePlatesFilterContent({
}
};
if (allRecognizedLicensePlates && allRecognizedLicensePlates.length === 0) {
return null;
}
const filterItems = (value: string, search: string) => {
if (!search) return 1; // Show all items if no search input
// If wrapped in /.../, treat as raw regex
if (search.startsWith("/") && search.endsWith("/") && search.length > 2) {
try {
const regex = new RegExp(search.slice(1, -1), "i");
return regex.test(value) ? 1 : -1;
} catch {
return -1;
}
}
if (search.includes("*") || search.includes("?")) {
const escapedSearch = search
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
@ -943,6 +949,46 @@ export function RecognizedLicensePlatesFilterContent({
return value.toLowerCase().includes(search.toLowerCase()) ? 1 : -1;
};
const filteredPlates = useMemo(() => {
if (!allRecognizedLicensePlates) return [];
return allRecognizedLicensePlates.filter(
(plate) => filterItems(plate, inputValue) > 0,
);
}, [allRecognizedLicensePlates, inputValue]);
const handleSelectAllVisible = () => {
const allVisibleSelected = filteredPlates.every((plate) =>
selectedRecognizedLicensePlates.includes(plate),
);
let newSelected;
if (allVisibleSelected) {
// clear all
newSelected = selectedRecognizedLicensePlates.filter(
(plate) => !filteredPlates.includes(plate),
);
} else {
// select all
newSelected = Array.from(
new Set([...selectedRecognizedLicensePlates, ...filteredPlates]),
);
}
setSelectedRecognizedLicensePlates(newSelected);
setRecognizedLicensePlates(
newSelected.length === 0 ? undefined : newSelected,
);
};
const handleClearAll = () => {
setSelectedRecognizedLicensePlates([]);
setRecognizedLicensePlates(undefined);
};
if (allRecognizedLicensePlates && allRecognizedLicensePlates.length === 0) {
return null;
}
return (
<div className="overflow-x-hidden">
<DropdownMenuSeparator className="mb-3" />
@ -1010,6 +1056,30 @@ export function RecognizedLicensePlatesFilterContent({
<p className="mt-1 text-sm text-muted-foreground">
{t("recognizedLicensePlates.selectPlatesFromList")}
</p>
<div className="mt-2 flex items-center justify-between text-sm text-muted-foreground">
{filteredPlates.length > 0 &&
!filteredPlates.every((plate) =>
selectedRecognizedLicensePlates.includes(plate),
) ? (
<button
onClick={handleSelectAllVisible}
className="flex items-center gap-1 text-sm text-primary hover:underline"
>
<LuSquareCheck className="size-4" />
{t("recognizedLicensePlates.selectAll")}
</button>
) : null}
{selectedRecognizedLicensePlates.length > 0 && (
<button
onClick={handleClearAll}
className="flex items-center gap-1 text-sm text-primary hover:underline"
>
<LuX className="size-4" />
{t("recognizedLicensePlates.clearAll")}
</button>
)}
</div>
</>
)}
</div>