mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-06-19 02:38:49 -04:00
fix: rank exact smart filter matches first (#557)
This commit is contained in:
2
deno.lock
generated
2
deno.lock
generated
@@ -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"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ export interface FilterTag {
|
||||
field: string;
|
||||
value: string;
|
||||
negated: boolean;
|
||||
exact?: boolean;
|
||||
}
|
||||
|
||||
export type SerializedFilterTag = Omit<FilterTag, 'id'>;
|
||||
|
||||
Reference in New Issue
Block a user