Cleanup event filters (#10724)

* Add specific button / switch for showing reviewed items and use intermediate drawer for mobile

* Match design for filters
This commit is contained in:
Nicolas Mowen 2024-03-28 08:43:05 -06:00 committed by GitHub
parent 35ecb342bb
commit 985b2d7b27
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 186 additions and 114 deletions

View File

@ -13,19 +13,35 @@ import {
import { ReviewFilter, ReviewSummary } from "@/types/review"; import { ReviewFilter, ReviewSummary } from "@/types/review";
import { getEndOfDayTimestamp } from "@/utils/dateUtil"; import { getEndOfDayTimestamp } from "@/utils/dateUtil";
import { useFormattedTimestamp } from "@/hooks/use-date-utils"; import { useFormattedTimestamp } from "@/hooks/use-date-utils";
import { FaCalendarAlt, FaFilter, FaRunning, FaVideo } from "react-icons/fa"; import {
import { isMobile } from "react-device-detect"; FaCalendarAlt,
FaCheckCircle,
FaFilter,
FaRunning,
FaVideo,
} from "react-icons/fa";
import { isDesktop, isMobile } from "react-device-detect";
import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer"; import { Drawer, DrawerContent, DrawerTrigger } from "../ui/drawer";
import { Switch } from "../ui/switch"; import { Switch } from "../ui/switch";
import { Label } from "../ui/label"; import { Label } from "../ui/label";
import FilterCheckBox from "./FilterCheckBox"; import FilterCheckBox from "./FilterCheckBox";
import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar"; import ReviewActivityCalendar from "../overlay/ReviewActivityCalendar";
import MobileReviewSettingsDrawer, {
DrawerFeatures,
} from "../overlay/MobileReviewSettingsDrawer";
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"]; const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
const REVIEW_FILTERS = ["cameras", "date", "general", "motionOnly"] as const; const REVIEW_FILTERS = [
"cameras",
"reviewed",
"date",
"general",
"motionOnly",
] as const;
type ReviewFilters = (typeof REVIEW_FILTERS)[number]; type ReviewFilters = (typeof REVIEW_FILTERS)[number];
const DEFAULT_REVIEW_FILTERS: ReviewFilters[] = [ const DEFAULT_REVIEW_FILTERS: ReviewFilters[] = [
"cameras", "cameras",
"reviewed",
"date", "date",
"general", "general",
"motionOnly", "motionOnly",
@ -94,6 +110,20 @@ export default function ReviewFilterGroup({
); );
}, [config]); }, [config]);
const mobileSettingsFeatures = useMemo<DrawerFeatures[]>(() => {
const features: DrawerFeatures[] = [];
if (filters.includes("date")) {
features.push("calendar");
}
if (filters.includes("general")) {
features.push("filter");
}
return features;
}, [filters]);
// handle updating filters // handle updating filters
const onUpdateSelectedDay = useCallback( const onUpdateSelectedDay = useCallback(
@ -119,7 +149,15 @@ export default function ReviewFilterGroup({
}} }}
/> />
)} )}
{filters.includes("date") && ( {filters.includes("reviewed") && (
<ShowReviewFilter
showReviewed={filter?.showReviewed || 0}
setShowReviewed={(reviewed) =>
onUpdateFilter({ ...filter, showReviewed: reviewed })
}
/>
)}
{isDesktop && filters.includes("date") && (
<CalendarFilterButton <CalendarFilterButton
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
day={ day={
@ -136,17 +174,27 @@ export default function ReviewFilterGroup({
setMotionOnly={setMotionOnly} setMotionOnly={setMotionOnly}
/> />
)} )}
{filters.includes("general") && ( {isDesktop && filters.includes("general") && (
<GeneralFilterButton <GeneralFilterButton
allLabels={filterValues.labels} allLabels={filterValues.labels}
selectedLabels={filter?.labels} selectedLabels={filter?.labels}
updateLabelFilter={(newLabels) => { updateLabelFilter={(newLabels) => {
onUpdateFilter({ ...filter, labels: newLabels }); onUpdateFilter({ ...filter, labels: newLabels });
}} }}
showReviewed={filter?.showReviewed || 0} />
setShowReviewed={(reviewed) => )}
onUpdateFilter({ ...filter, showReviewed: reviewed }) {isMobile && mobileSettingsFeatures.length > 0 && (
} <MobileReviewSettingsDrawer
features={mobileSettingsFeatures}
filter={filter}
onUpdateFilter={onUpdateFilter}
// not applicable as exports are not used
camera=""
latestTime={0}
currentTime={0}
mode="none"
setMode={() => {}}
setRange={() => {}}
/> />
)} )}
</div> </div>
@ -307,6 +355,41 @@ function CamerasFilterButton({
); );
} }
type ShowReviewedFilterProps = {
showReviewed?: 0 | 1;
setShowReviewed: (reviewed?: 0 | 1) => void;
};
function ShowReviewFilter({
showReviewed,
setShowReviewed,
}: ShowReviewedFilterProps) {
return (
<>
<div className="hidden h-9 md:flex p-2 justify-start items-center text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground rounded-md cursor-pointer">
<Switch
id="reviewed"
checked={showReviewed == 1}
onCheckedChange={() => setShowReviewed(showReviewed == 0 ? 1 : 0)}
/>
<Label className="ml-2 cursor-pointer" htmlFor="reviewed">
Show Reviewed
</Label>
</div>
<Button
className="block md:hidden ml-1"
size="sm"
variant="secondary"
onClick={() => setShowReviewed(showReviewed == 0 ? 1 : 0)}
>
<FaCheckCircle
className={`${showReviewed == 1 ? "text-selected" : "text-muted-foreground"}`}
/>
</Button>
</>
);
}
type CalendarFilterButtonProps = { type CalendarFilterButtonProps = {
reviewSummary?: ReviewSummary; reviewSummary?: ReviewSummary;
day?: Date; day?: Date;
@ -371,19 +454,14 @@ function CalendarFilterButton({
type GeneralFilterButtonProps = { type GeneralFilterButtonProps = {
allLabels: string[]; allLabels: string[];
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
showReviewed?: 0 | 1;
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
setShowReviewed: (reviewed?: 0 | 1) => void;
}; };
function GeneralFilterButton({ function GeneralFilterButton({
allLabels, allLabels,
selectedLabels, selectedLabels,
showReviewed,
updateLabelFilter, updateLabelFilter,
setShowReviewed,
}: GeneralFilterButtonProps) { }: GeneralFilterButtonProps) {
const [open, setOpen] = useState(false); const [open, setOpen] = useState(false);
const [reviewed, setReviewed] = useState(showReviewed ?? 0);
const [currentLabels, setCurrentLabels] = useState<string[] | undefined>( const [currentLabels, setCurrentLabels] = useState<string[] | undefined>(
selectedLabels, selectedLabels,
); );
@ -399,12 +477,8 @@ function GeneralFilterButton({
allLabels={allLabels} allLabels={allLabels}
selectedLabels={selectedLabels} selectedLabels={selectedLabels}
currentLabels={currentLabels} currentLabels={currentLabels}
showReviewed={showReviewed}
reviewed={reviewed}
updateLabelFilter={updateLabelFilter} updateLabelFilter={updateLabelFilter}
setShowReviewed={setShowReviewed}
setCurrentLabels={setCurrentLabels} setCurrentLabels={setCurrentLabels}
setReviewed={setReviewed}
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
/> />
); );
@ -415,7 +489,6 @@ function GeneralFilterButton({
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setReviewed(showReviewed ?? 0);
setCurrentLabels(selectedLabels); setCurrentLabels(selectedLabels);
} }
@ -435,7 +508,6 @@ function GeneralFilterButton({
open={open} open={open}
onOpenChange={(open) => { onOpenChange={(open) => {
if (!open) { if (!open) {
setReviewed(showReviewed ?? 0);
setCurrentLabels(selectedLabels); setCurrentLabels(selectedLabels);
} }
@ -443,7 +515,7 @@ function GeneralFilterButton({
}} }}
> >
<PopoverTrigger asChild>{trigger}</PopoverTrigger> <PopoverTrigger asChild>{trigger}</PopoverTrigger>
<PopoverContent side="left">{content}</PopoverContent> <PopoverContent>{content}</PopoverContent>
</Popover> </Popover>
); );
} }
@ -452,87 +524,84 @@ type GeneralFilterContentProps = {
allLabels: string[]; allLabels: string[];
selectedLabels: string[] | undefined; selectedLabels: string[] | undefined;
currentLabels: string[] | undefined; currentLabels: string[] | undefined;
showReviewed?: 0 | 1;
reviewed: 0 | 1;
updateLabelFilter: (labels: string[] | undefined) => void; updateLabelFilter: (labels: string[] | undefined) => void;
setCurrentLabels: (labels: string[] | undefined) => void; setCurrentLabels: (labels: string[] | undefined) => void;
setShowReviewed: (reviewed?: 0 | 1) => void;
setReviewed: (reviewed: 0 | 1) => void;
onClose: () => void; onClose: () => void;
}; };
export function GeneralFilterContent({ export function GeneralFilterContent({
allLabels, allLabels,
selectedLabels, selectedLabels,
currentLabels, currentLabels,
showReviewed,
reviewed,
updateLabelFilter, updateLabelFilter,
setCurrentLabels, setCurrentLabels,
setShowReviewed,
setReviewed,
onClose, onClose,
}: GeneralFilterContentProps) { }: GeneralFilterContentProps) {
return ( return (
<> <>
<div className="flex p-2 justify-start items-center">
<Switch
id="reviewed"
checked={reviewed == 1}
onCheckedChange={() => setReviewed(reviewed == 0 ? 1 : 0)}
/>
<Label className="ml-2" htmlFor="reviewed">
Show Reviewed
</Label>
</div>
<DropdownMenuSeparator />
<DropdownMenuLabel className="flex justify-center items-center">
Filter Labels
</DropdownMenuLabel>
<DropdownMenuSeparator />
<div className="h-auto overflow-y-auto overflow-x-hidden"> <div className="h-auto overflow-y-auto overflow-x-hidden">
<FilterCheckBox <div className="flex justify-between items-center my-2.5">
isChecked={currentLabels == undefined} <Label
label="All Labels" className="mx-2 text-secondary-foreground cursor-pointer"
onCheckedChange={(isChecked) => { htmlFor="allLabels"
if (isChecked) { >
setCurrentLabels(undefined); All Labels
} </Label>
}} <Switch
/> className="ml-1"
<DropdownMenuSeparator /> id="allLabels"
{allLabels.map((item) => ( checked={currentLabels == undefined}
<FilterCheckBox
key={item}
isChecked={currentLabels?.includes(item) ?? false}
label={item.replaceAll("_", " ")}
onCheckedChange={(isChecked) => { onCheckedChange={(isChecked) => {
if (isChecked) { if (isChecked) {
const updatedLabels = currentLabels ? [...currentLabels] : []; setCurrentLabels(undefined);
updatedLabels.push(item);
setCurrentLabels(updatedLabels);
} else {
const updatedLabels = currentLabels ? [...currentLabels] : [];
// can not deselect the last item
if (updatedLabels.length > 1) {
updatedLabels.splice(updatedLabels.indexOf(item), 1);
setCurrentLabels(updatedLabels);
}
} }
}} }}
/> />
))} </div>
<DropdownMenuSeparator />
<div className="my-2.5 flex flex-col gap-2.5">
{allLabels.map((item) => (
<div className="flex justify-between items-center">
<Label
className="w-full mx-2 text-secondary-foreground capitalize cursor-pointer"
htmlFor={item}
>
{item.replaceAll("_", " ")}
</Label>
<Switch
key={item}
className="ml-1"
id={item}
checked={currentLabels?.includes(item) ?? false}
onCheckedChange={(isChecked) => {
if (isChecked) {
const updatedLabels = currentLabels
? [...currentLabels]
: [];
updatedLabels.push(item);
setCurrentLabels(updatedLabels);
} else {
const updatedLabels = currentLabels
? [...currentLabels]
: [];
// can not deselect the last item
if (updatedLabels.length > 1) {
updatedLabels.splice(updatedLabels.indexOf(item), 1);
setCurrentLabels(updatedLabels);
}
}
}}
/>
</div>
))}
</div>
</div> </div>
<DropdownMenuSeparator /> <DropdownMenuSeparator />
<div className="p-2 flex justify-evenly items-center"> <div className="p-2 flex justify-evenly items-center">
<Button <Button
variant="select" variant="select"
onClick={() => { onClick={() => {
if (reviewed != showReviewed) {
setShowReviewed(reviewed);
}
if (selectedLabels != currentLabels) { if (selectedLabels != currentLabels) {
updateLabelFilter(currentLabels); updateLabelFilter(currentLabels);
} }
@ -545,8 +614,6 @@ export function GeneralFilterContent({
<Button <Button
variant="secondary" variant="secondary"
onClick={() => { onClick={() => {
setReviewed(0);
setShowReviewed(undefined);
setCurrentLabels(undefined); setCurrentLabels(undefined);
updateLabelFilter(undefined); updateLabelFilter(undefined);
}} }}
@ -568,7 +635,7 @@ function ShowMotionOnlyButton({
}: ShowMotionOnlyButtonProps) { }: ShowMotionOnlyButtonProps) {
return ( return (
<> <>
<div className="hidden md:inline-flex items-center justify-center whitespace-nowrap text-sm bg-secondary text-secondary-foreground h-9 rounded-md md:px-3 md:mx-1"> <div className="hidden md:inline-flex items-center justify-center whitespace-nowrap text-sm bg-secondary hover:bg-secondary/80 text-secondary-foreground h-9 rounded-md px-3 mx-1 cursor-pointer">
<Switch <Switch
className="ml-1" className="ml-1"
id="collapse-motion" id="collapse-motion"
@ -578,7 +645,7 @@ function ShowMotionOnlyButton({
}} }}
/> />
<Label <Label
className="mx-2 text-secondary-foreground" className="mx-2 text-secondary-foreground cursor-pointer"
htmlFor="collapse-motion" htmlFor="collapse-motion"
> >
Motion only Motion only

View File

@ -20,7 +20,16 @@ import { isMobile } from "react-device-detect";
const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"]; const ATTRIBUTES = ["amazon", "face", "fedex", "license_plate", "ups"];
type DrawerMode = "none" | "select" | "export" | "calendar" | "filter"; type DrawerMode = "none" | "select" | "export" | "calendar" | "filter";
const DRAWER_FEATURES = ["export", "calendar", "filter"] as const;
export type DrawerFeatures = (typeof DRAWER_FEATURES)[number];
const DEFAULT_DRAWER_FEATURES: DrawerFeatures[] = [
"export",
"calendar",
"filter",
];
type MobileReviewSettingsDrawerProps = { type MobileReviewSettingsDrawerProps = {
features?: DrawerFeatures[];
camera: string; camera: string;
filter?: ReviewFilter; filter?: ReviewFilter;
latestTime: number; latestTime: number;
@ -32,6 +41,7 @@ type MobileReviewSettingsDrawerProps = {
setMode: (mode: ExportMode) => void; setMode: (mode: ExportMode) => void;
}; };
export default function MobileReviewSettingsDrawer({ export default function MobileReviewSettingsDrawer({
features = DEFAULT_DRAWER_FEATURES,
camera, camera,
filter, filter,
latestTime, latestTime,
@ -123,27 +133,33 @@ export default function MobileReviewSettingsDrawer({
if (drawerMode == "select") { if (drawerMode == "select") {
content = ( content = (
<div className="w-full p-4 flex flex-col gap-2"> <div className="w-full p-4 flex flex-col gap-2">
<Button {features.includes("export") && (
className="w-full flex justify-center items-center gap-2" <Button
onClick={() => setDrawerMode("export")} className="w-full flex justify-center items-center gap-2"
> onClick={() => setDrawerMode("export")}
<FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" /> >
Export <FaArrowDown className="p-1 fill-secondary bg-muted-foreground rounded-md" />
</Button> Export
<Button </Button>
className="w-full flex justify-center items-center gap-2" )}
onClick={() => setDrawerMode("calendar")} {features.includes("calendar") && (
> <Button
<FaCalendarAlt className="fill-muted-foreground" /> className="w-full flex justify-center items-center gap-2"
Calendar onClick={() => setDrawerMode("calendar")}
</Button> >
<Button <FaCalendarAlt className="fill-muted-foreground" />
className="w-full flex justify-center items-center gap-2" Calendar
onClick={() => setDrawerMode("filter")} </Button>
> )}
<FaFilter className="fill-muted-foreground" /> {features.includes("filter") && (
Filter <Button
</Button> className="w-full flex justify-center items-center gap-2"
onClick={() => setDrawerMode("filter")}
>
<FaFilter className="fill-muted-foreground" />
Filter
</Button>
)}
</div> </div>
); );
} else if (drawerMode == "export") { } else if (drawerMode == "export") {
@ -230,17 +246,13 @@ export default function MobileReviewSettingsDrawer({
</div> </div>
</div> </div>
<GeneralFilterContent <GeneralFilterContent
allLabels={allLabels.concat(allLabels)} allLabels={allLabels}
selectedLabels={filter?.labels} selectedLabels={filter?.labels}
currentLabels={currentLabels} currentLabels={currentLabels}
showReviewed={0}
reviewed={0}
setCurrentLabels={setCurrentLabels} setCurrentLabels={setCurrentLabels}
updateLabelFilter={(newLabels) => updateLabelFilter={(newLabels) =>
onUpdateFilter({ ...filter, labels: newLabels }) onUpdateFilter({ ...filter, labels: newLabels })
} }
setShowReviewed={() => {}}
setReviewed={() => {}}
onClose={() => setDrawerMode("select")} onClose={() => setDrawerMode("select")}
/> />
</div> </div>
@ -280,10 +292,3 @@ export default function MobileReviewSettingsDrawer({
</> </>
); );
} }
/**
* <MobileTimelineDrawer
selected={timelineType ?? "timeline"}
onSelect={setTimelineType}
/>
*/

View File

@ -257,7 +257,7 @@ export default function EventView({
filters={ filters={
severity == "significant_motion" severity == "significant_motion"
? ["cameras", "date", "motionOnly"] ? ["cameras", "date", "motionOnly"]
: ["cameras", "date", "general"] : ["cameras", "reviewed", "date", "general"]
} }
reviewSummary={reviewSummary} reviewSummary={reviewSummary}
filter={filter} filter={filter}