From be3e1831d4a92e02cf7d28bd0305fef9610a4a5f Mon Sep 17 00:00:00 2001 From: Josh Hawkins <32435876+hawkeye217@users.noreply.github.com> Date: Wed, 25 Sep 2024 12:15:08 -0500 Subject: [PATCH] Add ability to use 12 hour time in input time filter (#13961) --- web/src/components/input/InputWithTags.tsx | 86 ++++++++++++---------- web/src/utils/dateUtil.ts | 44 +++++++++-- web/src/views/search/SearchView.tsx | 5 +- 3 files changed, 91 insertions(+), 44 deletions(-) diff --git a/web/src/components/input/InputWithTags.tsx b/web/src/components/input/InputWithTags.tsx index 062a38d13..9d4632885 100644 --- a/web/src/components/input/InputWithTags.tsx +++ b/web/src/components/input/InputWithTags.tsx @@ -38,6 +38,7 @@ import { convertTo12Hour, getIntlDateFormat, isValidTimeRange, + to24Hour, } from "@/utils/dateUtil"; import { toast } from "sonner"; import useSWR from "swr"; @@ -191,7 +192,8 @@ export default function InputWithTags({ if ( allSuggestions[type as FilterType]?.includes(value) || type == "before" || - type == "after" + type == "after" || + type == "time_range" ) { const newFilters = { ...filters }; let timestamp = 0; @@ -235,23 +237,6 @@ export default function InputWithTags({ } break; case "time_range": - if (!value.includes(",")) { - toast.error( - "The correct format is after,before. Example: 15:00,18:00.", - { - position: "top-center", - }, - ); - return; - } - - if (!isValidTimeRange(value)) { - toast.error("Time range is not valid.", { - position: "top-center", - }); - return; - } - newFilters[type] = value; break; case "search_type": @@ -299,7 +284,9 @@ export default function InputWithTags({ : (filterValues as number)) * 1000, ).toLocaleDateString(window.navigator?.language || "en-US"); } else if (filterType === "time_range") { - const [startTime, endTime] = (filterValues as string).split(","); + const [startTime, endTime] = (filterValues as string) + .replace("-", ",") + .split(","); return `${ config?.ui.time_format === "24hour" ? startTime @@ -320,9 +307,23 @@ export default function InputWithTags({ if ( allSuggestions[filterType]?.includes(trimmedValue) || ((filterType === "before" || filterType === "after") && - trimmedValue.match(/^\d{8}$/)) + trimmedValue.match(/^\d{8}$/)) || + (filterType === "time_range" && + isValidTimeRange( + trimmedValue.replace("-", ","), + config?.ui.time_format, + )) ) { - createFilter(filterType, trimmedValue); + createFilter( + filterType, + filterType === "time_range" + ? trimmedValue + .replace("-", ",") + .split(",") + .map((time) => to24Hour(time.trim(), config?.ui.time_format)) + .join(",") + : trimmedValue, + ); setInputValue((prev) => { const regex = new RegExp( `${filterType}:${filterValue.trim()}[,\\s]*`, @@ -335,7 +336,7 @@ export default function InputWithTags({ setCurrentFilterType(null); } }, - [allSuggestions, createFilter], + [allSuggestions, createFilter, config], ); const handleInputChange = useCallback( @@ -362,18 +363,11 @@ export default function InputWithTags({ if (filterType in allSuggestions) { setCurrentFilterType(filterType); - if (filterType === "before" || filterType === "after") { - // For before and after, we don't need to update suggestions - if (filterValue.match(/^\d{8}$/)) { - handleFilterCreation(filterType, filterValue); - } - } else { - updateSuggestions(filterValue, filterType); + updateSuggestions(filterValue, filterType); - // Check if the last character is a space or comma - if (isLastCharSpaceOrComma) { - handleFilterCreation(filterType, filterValue); - } + // Check if the last character is a space or comma + if (isLastCharSpaceOrComma) { + handleFilterCreation(filterType, filterValue); } } else { resetSuggestions(value); @@ -413,6 +407,13 @@ export default function InputWithTags({ (suggestion: string) => { if (currentFilterType) { // Apply the selected suggestion to the current filter type + if (currentFilterType == "time_range") { + suggestion = suggestion + .replace("-", ",") + .split(",") + .map((time) => to24Hour(time.trim(), config?.ui.time_format)) + .join(","); + } createFilter(currentFilterType, suggestion); setInputValue((prev) => { const regex = new RegExp(`${currentFilterType}:[^\\s,]*`, "g"); @@ -439,7 +440,7 @@ export default function InputWithTags({ inputRef.current?.focus(); }, - [createFilter, currentFilterType, allSuggestions], + [createFilter, currentFilterType, allSuggestions, config], ); const handleSearch = useCallback( @@ -565,7 +566,7 @@ export default function InputWithTags({
Filters help you narrow down your search results. Here's how - to use them: + to use them in the input field:
cameras:front_door label:person before:01012024
+ time_range:3:00PM-4:00PM
diff --git a/web/src/utils/dateUtil.ts b/web/src/utils/dateUtil.ts
index 9e0b25c5c..dc5589884 100644
--- a/web/src/utils/dateUtil.ts
+++ b/web/src/utils/dateUtil.ts
@@ -387,22 +387,54 @@ export function formatDateToLocaleString(daysOffset: number = 0): string {
.replace(/[^\d]/g, "");
}
-export function isValidTimeRange(rangeString: string): boolean {
- const range = rangeString.split(",");
+export function to24Hour(
+ time: string,
+ time_format: "12hour" | "24hour" | "browser" = "24hour",
+): string {
+ const is24HourFormat = time_format === "24hour";
+ if (is24HourFormat) return time;
+
+ const [timePart, ampm] = time.split(/([AP]M)/i);
+
+ if (!timePart || !ampm) {
+ throw new Error(`Invalid time format: ${time}`);
+ }
+
+ let hours = Number(timePart.split(":")[0]);
+ const minutes = Number(timePart.split(":")[1]);
+
+ if (ampm.toUpperCase() === "PM" && hours !== 12) hours += 12;
+ if (ampm.toUpperCase() === "AM" && hours === 12) hours = 0;
+
+ return `${hours.toString().padStart(2, "0")}:${minutes.toString().padStart(2, "0")}`;
+}
+
+export function isValidTimeRange(
+ rangeString: string,
+ time_format?: "12hour" | "24hour" | "browser",
+): boolean {
+ const range = rangeString.split(",");
if (range.length !== 2) {
return false;
}
+ const is24HourFormat = time_format === "24hour";
+
const toMinutes = (time: string): number => {
- const [h, m] = time.split(":").map(Number);
+ const [h, m] = to24Hour(time, time_format).split(":").map(Number);
return h * 60 + m;
};
- const isValidTime = (time: string): boolean =>
- /^(?:([01]\d|2[0-3]):([0-5]\d)|24:00)$/.test(time);
+ const isValidTime = (time: string): boolean => {
+ if (is24HourFormat) {
+ return /^(?:([01]\d|2[0-3]):([0-5]\d)|24:00)$/.test(time);
+ } else {
+ return /^(0?[1-9]|1[0-2]):[0-5][0-9](A|P)M$/i.test(time);
+ }
+ };
- const [startTime, endTime] = range;
+ const [startTime, endTime] = range.map((t) => t.trim());
return (
isValidTime(startTime) &&
diff --git a/web/src/views/search/SearchView.tsx b/web/src/views/search/SearchView.tsx
index 0a41163f6..28f9ed579 100644
--- a/web/src/views/search/SearchView.tsx
+++ b/web/src/views/search/SearchView.tsx
@@ -121,7 +121,10 @@ export default function SearchView({
zones: Object.values(allZones || {}),
sub_labels: allSubLabels,
search_type: ["thumbnail", "description"] as SearchSource[],
- time_range: ["00:00,24:00"],
+ time_range:
+ config?.ui.time_format == "24hour"
+ ? ["00:00-23:59"]
+ : ["12:00AM-11:59PM"],
before: [formatDateToLocaleString()],
after: [formatDateToLocaleString(-5)],
}),