diff --git a/deno.lock b/deno.lock index d3a39963..56823776 100644 --- a/deno.lock +++ b/deno.lock @@ -1524,7 +1524,7 @@ "npm:svelte@^5.55.5", "npm:tailwindcss@^4.1.13", "npm:typescript@^6.0.3", - "npm:vite@^8.0.10", + "npm:vite@^8.0.11", "npm:yaml@^2.8.4" ] } diff --git a/src/lib/client/ui/filter/SmartFilterBar.svelte b/src/lib/client/ui/filter/SmartFilterBar.svelte index e511c9f0..04fb2270 100644 --- a/src/lib/client/ui/filter/SmartFilterBar.svelte +++ b/src/lib/client/ui/filter/SmartFilterBar.svelte @@ -46,7 +46,12 @@ const stored = localStorage.getItem(storageKey); if (stored) { const parsed: SerializedFilterTag[] = JSON.parse(stored); - return parsed.map((t) => ({ ...t, id: uuid() })); + return parsed.map((t) => ({ + id: uuid(), + field: t.field, + value: t.value, + negated: t.negated + })); } } catch {} return []; @@ -211,20 +216,10 @@ onchange?.(newTags); } - function addTag(field: string, value: string, exact?: boolean) { + function addTag(field: string, value: string) { const trimmed = value.trim(); if (!trimmed) return; - let resolvedExact = exact; - if (resolvedExact === undefined) { - const fieldDef = fieldMap.get(field); - const fieldSuggestions = fieldDef?.suggestions?.(items) ?? []; - const trimmedLower = trimmed.toLowerCase(); - resolvedExact = fieldSuggestions.some((s) => s.toLowerCase() === trimmedLower); - } - updateTags([ - ...tags, - { id: uuid(), field, value: trimmed, negated: false, exact: resolvedExact } - ]); + updateTags([...tags, { id: uuid(), field, value: trimmed, negated: false }]); inputValue = ''; valueInputValue = ''; activeFieldDef = null; @@ -293,7 +288,7 @@ const field = fieldMap.get(selected.value); if (field) selectField(field); } else { - // No field match — create tag with default field + // No field match: create tag with default field const trimmed = inputValue.trim(); if (trimmed && defaultField) { addTag(defaultField.key, trimmed); @@ -301,7 +296,7 @@ } } else if (phase === 'value' && activeFieldDef) { if (highlightedIndex >= 0 && highlightedIndex < suggestions.length) { - addTag(activeFieldDef.key, suggestions[highlightedIndex].value, true); + addTag(activeFieldDef.key, suggestions[highlightedIndex].value); } else if (valueInputValue.trim()) { addTag(activeFieldDef.key, valueInputValue); } @@ -314,7 +309,7 @@ const field = fieldMap.get(selected.value); if (field) selectField(field); } else if (phase === 'value' && activeFieldDef) { - addTag(activeFieldDef.key, suggestions[index].value, true); + addTag(activeFieldDef.key, suggestions[index].value); } } diff --git a/src/lib/client/ui/filter/match.ts b/src/lib/client/ui/filter/match.ts index c3297cf6..81db79f6 100644 --- a/src/lib/client/ui/filter/match.ts +++ b/src/lib/client/ui/filter/match.ts @@ -55,6 +55,12 @@ function matchesNumberCondition(actual: number, condition: NumberCondition): boo } } +function stringValues(rawValue: string | number | string[] | null): string[] { + if (rawValue == null) return []; + if (Array.isArray(rawValue)) return rawValue; + return [String(rawValue)]; +} + export function applySmartFilters( items: T[], tags: FilterTag[], @@ -64,35 +70,41 @@ export function applySmartFilters( const fieldMap = new Map(fields.map((f) => [f.key, f])); - return items.filter((item) => { - return tags.every((tag) => { - const field = fieldMap.get(tag.field); - if (!field) return true; + const matchedItems = items + .map((item, index) => { + let exactRank = 0; + const matchesAllTags = tags.every((tag) => { + const field = fieldMap.get(tag.field); + if (!field) return true; - const rawValue = field.accessor(item); + const rawValue = field.accessor(item); - let matches: boolean; + let matches: boolean; - if (field.type === 'number') { - const condition = parseNumberCondition(tag.value); - if (!condition || rawValue == null) return !tag.negated; - matches = matchesNumberCondition(Number(rawValue), condition); - } else { - const tagLower = tag.value.toLowerCase(); - if (rawValue == null) { - matches = false; - } else if (tag.exact) { - matches = Array.isArray(rawValue) - ? rawValue.some((v) => v.toLowerCase() === tagLower) - : String(rawValue).toLowerCase() === tagLower; - } else if (Array.isArray(rawValue)) { - matches = rawValue.some((v) => v.toLowerCase().includes(tagLower)); + if (field.type === 'number') { + const condition = parseNumberCondition(tag.value); + if (!condition || rawValue == null) return !tag.negated; + matches = matchesNumberCondition(Number(rawValue), condition); } else { - matches = String(rawValue).toLowerCase().includes(tagLower); - } - } + const tagLower = tag.value.toLowerCase(); + const values = stringValues(rawValue); + const hasExactMatch = values.some((v) => v.toLowerCase() === tagLower); - return tag.negated ? !matches : matches; - }); - }); + matches = values.some((v) => v.toLowerCase().includes(tagLower)); + + if (matches && !tag.negated && hasExactMatch) { + exactRank += 1; + } + } + + return tag.negated ? !matches : matches; + }); + + return matchesAllTags ? { item, index, exactRank } : null; + }) + .filter((result): result is { item: T; index: number; exactRank: number } => result !== null); + + return matchedItems + .sort((a, b) => b.exactRank - a.exactRank || a.index - b.index) + .map((result) => result.item); } diff --git a/src/lib/client/ui/filter/types.ts b/src/lib/client/ui/filter/types.ts index 9c8877c3..1efcc9a3 100644 --- a/src/lib/client/ui/filter/types.ts +++ b/src/lib/client/ui/filter/types.ts @@ -15,7 +15,6 @@ export interface FilterTag { field: string; value: string; negated: boolean; - exact?: boolean; } export type SerializedFilterTag = Omit;