Searching is hard

This commit is contained in:
dirwin31
2026-08-27 17:44:39 -07:00
parent a0aa48f82a
commit f07d588534
3 changed files with 66 additions and 14 deletions
@@ -110,6 +110,14 @@ function refresh() {
return fetchRoutes()
}
function changeSortOrder(event) {
const sortOrder = String(event.target.value || "")
if (!["newest", "oldest", "longest", "shortest"].includes(sortOrder)) return
state.sortOrder = sortOrder
// Replacing the array forces the keyed route templates to move immediately.
state.routes = [...state.routes]
}
if (!isGalaxyTunnel()) refresh()
function openDialog(htmlString) {
@@ -825,7 +833,7 @@ export function RouteRecordings() {
</div>
<label class="dashcam-sort">
<span>Sort</span>
<select value="${() => state.sortOrder}" @change="${event => { state.sortOrder = event.target.value }}">
<select value="${() => state.sortOrder}" @input="${changeSortOrder}" @change="${changeSortOrder}">
<option value="newest">Newest first</option>
<option value="oldest">Oldest first</option>
<option value="longest">Longest duration</option>
@@ -1,4 +1,5 @@
export const MAX_RENDERED_ROUTES = 250
const SEARCH_MONTHS = ["january", "february", "march", "april", "may", "june", "july", "august", "september", "october", "november", "december"]
function validDate(value) {
if (!value) return null
@@ -55,12 +56,12 @@ export function normalizeRoute(route, locale) {
displayDate,
displayName,
_startedAtMs: startedAtDate?.getTime() ?? timestampDate?.getTime() ?? null,
_searchValues: [
normalizeRouteSearchText(route?.name),
normalizeRouteSearchText(isCustomName ? displayName : ""),
...dateAliases.map(normalizeRouteSearchText),
...timeAliases.map(normalizeRouteSearchText),
].filter(Boolean),
_searchIndex: {
ids: [normalizeRouteSearchText(route?.name)].filter(Boolean),
titles: [normalizeRouteSearchText(isCustomName ? displayName : "")].filter(Boolean),
dates: dateAliases.map(normalizeRouteSearchText).filter(Boolean),
times: timeAliases.map(normalizeRouteSearchText).filter(Boolean),
},
}
}
@@ -126,16 +127,37 @@ export function sortRoutes(routes, sortOrder = "newest") {
}
export function routeMatchesSearch(route, searchQuery) {
const rawQuery = String(searchQuery || "").trim()
const normalizedQuery = normalizeRouteSearchText(searchQuery)
const queryTokens = normalizedQuery.split(" ").filter(Boolean)
if (!queryTokens.length) return true
const searchValues = Array.isArray(route?._searchValues) ? route._searchValues : [
route?.name,
route?.timestamp,
route?.displayName,
route?.displayDate,
].map(normalizeRouteSearchText).filter(Boolean)
const fallbackIndex = {
ids: [route?.name],
titles: [route?.timestamp, route?.displayName],
dates: [route?.displayDate],
times: [route?.displayDate],
}
const searchIndex = route?._searchIndex || Object.fromEntries(
Object.entries(fallbackIndex).map(([key, values]) => [key, values.map(normalizeRouteSearchText).filter(Boolean)]),
)
const hasMonth = queryTokens.some(token => token.length >= 3 && SEARCH_MONTHS.some(month => month.startsWith(token)))
const isDateQuery = hasMonth || /\d\s*[/-]\s*\d/.test(rawQuery) || /\d+(?:st|nd|rd|th)\b/i.test(rawQuery) || /^\d{4}$/.test(normalizedQuery)
const isTimeQuery = /^\d{1,2}$/.test(normalizedQuery) || /\d\s*:\s*\d/.test(rawQuery) || queryTokens.some(token => token === "am" || token === "pm")
const compactQuery = normalizedQuery.replaceAll(" ", "")
const isHexQuery = /^[0-9a-f]+$/.test(compactQuery)
const isIdQuery = rawQuery.includes("--") || (isHexQuery && (
compactQuery.length >= 8 || (compactQuery.length >= 4 && /\d/.test(compactQuery) && /[a-f]/.test(compactQuery))
))
const valuesFor = key => Array.isArray(searchIndex[key]) ? searchIndex[key] : []
const searchValues = [
...valuesFor("titles"),
...(isDateQuery ? valuesFor("dates") : []),
...(isTimeQuery ? valuesFor("times") : []),
...(isIdQuery ? valuesFor("ids") : []),
]
return searchValues.some(value => {
if (value.includes(normalizedQuery)) return true
const searchTokens = value.split(" ").filter(Boolean)
@@ -82,7 +82,8 @@ def test_route_titles_and_custom_name_badges_are_reactive():
def test_sort_order_select_and_route_items_are_keyed_and_reactive():
source = COMPONENT_PATH.read_text(encoding="utf-8")
assert '<select value="${() => state.sortOrder}" @change="${event => { state.sortOrder = event.target.value }}">' in source
assert '<select value="${() => state.sortOrder}" @input="${changeSortOrder}" @change="${changeSortOrder}">' in source
assert "state.routes = [...state.routes]" in source
assert ".key(group.key)" in source
assert ".key(route.name)" in source
@@ -189,6 +190,27 @@ def test_search_indexes_the_displayed_time_for_every_route():
]
def test_short_numeric_search_does_not_match_hidden_ids_or_unrelated_dates():
result = evaluate('''
const routes = [
route("00000012--9f0a7bdf9c", "2026-08-27T13:05:00Z", { timestamp: "Morning drive", isCustomName: true }),
route("0000006b--9f0a7bdf9d", "2026-12-12T13:05:00Z", { timestamp: "Afternoon drive", isCustomName: true }),
route("0000006c--9f0a7bdf9e", "2026-08-27T13:05:00Z", { timestamp: "Test_12", isCustomName: true }),
]
return {
plainNumber: buildRouteView(routes, { searchQuery: "12" }).matching.map(item => item.name),
ordinalDate: buildRouteView(routes, { searchQuery: "dec 12th" }).matching.map(item => item.name),
explicitId: buildRouteView(routes, { searchQuery: "00000012" }).matching.map(item => item.name),
}
''')
assert result == {
"plainNumber": ["0000006c--9f0a7bdf9e"],
"ordinalDate": ["0000006b--9f0a7bdf9d"],
"explicitId": ["00000012--9f0a7bdf9c"],
}
def test_filters_preserved_routes_before_applying_the_render_limit():
view = evaluate('''
const routes = Array.from({ length: MAX_RENDERED_ROUTES + 25 }, (_, index) => route(