fix: rank exact smart filter matches first (#557)

This commit is contained in:
santiagosayshey
2026-05-08 15:32:59 +09:30
committed by GitHub
parent 936bb9347f
commit 148fffcd40
4 changed files with 50 additions and 44 deletions

2
deno.lock generated
View File

@@ -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"
]
}

View File

@@ -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);
}
}

View File

@@ -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<T>(
items: T[],
tags: FilterTag[],
@@ -64,35 +70,41 @@ export function applySmartFilters<T>(
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);
}

View File

@@ -15,7 +15,6 @@ export interface FilterTag {
field: string;
value: string;
negated: boolean;
exact?: boolean;
}
export type SerializedFilterTag = Omit<FilterTag, 'id'>;