refactor(web): share dropdown search matching

This commit is contained in:
Dariusz L
2026-08-06 17:01:21 +02:00
parent 6f35df33d6
commit eb430f16a0
5 changed files with 62 additions and 20 deletions
+37
View File
@@ -0,0 +1,37 @@
import fs from 'node:fs';
import path from 'node:path';
import test from 'node:test';
import assert from 'node:assert/strict';
import { matchesSearchText, normalizeSearchToken } from '../web/resolver/utils/search_utils.js';
import { baseModelAliasMethods } from '../web/resolver/search/base_model_alias_methods.js';
const projectRoot = path.resolve(import.meta.dirname, '..');
const downloadTargetMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/search/download_target_methods.js'),
'utf8'
);
const resolveDownloadMethodsSource = fs.readFileSync(
path.join(projectRoot, 'web/resolver/actions/resolve_download_methods.js'),
'utf8'
);
test('search text matching preserves raw, normalized, and empty-query behavior', () => {
assert.equal(matchesSearchText('Checkpoints Check Points', ''), true);
assert.equal(matchesSearchText('Flux.1 D', 'flux'), true);
assert.equal(matchesSearchText('Flux.1 D', 'flux1d'), true);
assert.equal(matchesSearchText('Flux.1 D', 'flux-1-d'), true);
assert.equal(matchesSearchText('Flux.1 D', 'sdxl'), false);
assert.equal(matchesSearchText('Flux.1 D', '---'), false);
});
test('base model token normalization uses the shared search token contract', () => {
const dialog = { ...baseModelAliasMethods };
for (const value of ['', ' Flux.1 D ', 'SDXL 1.0', 'Pony XL']) {
assert.equal(dialog.normalizeBaseModelToken(value), normalizeSearchToken(value));
}
});
test('download category and base model dropdowns use the shared matcher', () => {
assert.match(downloadTargetMethodsSource, /matchesSearchText\(`\$\{option\.value\} \$\{option\.label\}`, filter\)/);
assert.match(resolveDownloadMethodsSource, /matchesSearchText\(`\$\{option\.value\} \$\{option\.label\}`, filter\)/);
});
@@ -6,6 +6,7 @@ import { normalizeSha256 } from "../utils/hash_utils.js";
import { normalizePathIdentity } from "../utils/html_utils.js";
import { bindEventOnce, syncElementAttributes } from "../utils/dom_patch_utils.js";
import { bindDownloadActionHandlers } from "../utils/download_action_handlers.js";
import { matchesSearchText } from "../utils/search_utils.js";
import { normalizeSourceKey } from "../utils/source_labels.js";
const log = createModuleLogger('resolve_download_methods');
@@ -3359,7 +3360,6 @@ export const resolveDownloadMethods = {
const renderBaseOptions = (filterText = '') => {
const filter = String(filterText || '').trim().toLowerCase();
const normalizedFilter = this.normalizeBaseModelToken?.(filter) || filter.replace(/[^a-z0-9]+/g, '');
const options = this.getKnownBaseModelOptions()
.map(option => {
const label = option.value === 'auto'
@@ -3367,14 +3367,7 @@ export const resolveDownloadMethods = {
: option.label;
return { ...option, label };
})
.filter(option => {
if (!filter) return true;
const searchText = `${option.value} ${option.label}`.toLowerCase();
const normalizedSearchText = this.normalizeBaseModelToken?.(searchText)
|| searchText.replace(/[^a-z0-9]+/g, '');
return searchText.includes(filter)
|| (normalizedFilter && normalizedSearchText.includes(normalizedFilter));
});
.filter(option => matchesSearchText(`${option.value} ${option.label}`, filter));
baseList.innerHTML = options
.map(option => {
const label = option.label;
@@ -1,3 +1,5 @@
import { normalizeSearchToken } from '../utils/search_utils.js';
export const baseModelAliasMethods = {
getBaseModelAliases() {
const baseModelsList = this.baseModels?.base_models;
@@ -22,9 +24,7 @@ export const baseModelAliasMethods = {
},
normalizeBaseModelToken(value = '') {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '');
return normalizeSearchToken(value);
},
getBaseModelTokenVariants(value = '') {
@@ -4,6 +4,7 @@ import { createFloatingTreePicker } from "../utils/tree_picker.js";
import { normalizeDownloadCategoryValue } from "../utils/category_utils.js";
import { safeStorage } from "../utils/html_utils.js";
import { getSha256Field } from "../utils/hash_utils.js";
import { matchesSearchText } from "../utils/search_utils.js";
const localStorage = safeStorage;
const invalidWindowsPathCharacters = new RegExp(
`[<>:"|?*${Array.from({ length: 32 }, (_, code) => String.fromCharCode(code)).join('')}]+`,
@@ -2856,19 +2857,12 @@ export const downloadTargetMethods = {
const populateCategoryOptions = (filterText = '') => {
if (!categoryListEl) return;
const filter = String(filterText || '').trim().toLowerCase();
const normalizedFilter = filter.replace(/[^a-z0-9]+/g, '');
const options = this.getDownloadCategoryOptions(this.getDropdownValue(categoryEl) || 'checkpoints')
.map(category => ({
value: category,
label: this.getCategoryDisplayName(category)
}))
.filter(option => {
if (!filter) return true;
const searchText = `${option.value} ${option.label}`.toLowerCase();
const normalizedSearchText = searchText.replace(/[^a-z0-9]+/g, '');
return searchText.includes(filter)
|| (normalizedFilter && normalizedSearchText.includes(normalizedFilter));
});
.filter(option => matchesSearchText(`${option.value} ${option.label}`, filter));
renderOptions(categoryListEl, options, (value, label) => {
this.setDropdownValue(categoryEl, value, label);
subfolderEl.value = '';
+18
View File
@@ -0,0 +1,18 @@
export function normalizeSearchToken(value = '') {
return String(value || '')
.toLowerCase()
.replace(/[^a-z0-9]+/g, '');
}
export function matchesSearchText(value = '', query = '') {
const filter = String(query || '').trim().toLowerCase();
if (!filter) return true;
const searchText = String(value || '').toLowerCase();
const normalizedFilter = normalizeSearchToken(filter);
return searchText.includes(filter)
|| Boolean(
normalizedFilter
&& normalizeSearchToken(searchText).includes(normalizedFilter)
);
}