From f5fbf5c5423c763796fa208190da0ac5ba63ca48 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Mon, 5 Oct 2026 20:05:19 +0200 Subject: [PATCH] Add searchable base model path mapping picker --- tests/test_base_model_mapping_picker.mjs | 44 +++++++++++++ web/resolver/views/options_methods.js | 78 ++++++++++++++++++------ 2 files changed, 102 insertions(+), 20 deletions(-) create mode 100644 tests/test_base_model_mapping_picker.mjs diff --git a/tests/test_base_model_mapping_picker.mjs b/tests/test_base_model_mapping_picker.mjs new file mode 100644 index 0000000..2538004 --- /dev/null +++ b/tests/test_base_model_mapping_picker.mjs @@ -0,0 +1,44 @@ +import fs from 'node:fs'; +import test from 'node:test'; +import assert from 'node:assert/strict'; +import { Window } from 'happy-dom'; + +test('mapping picker filters models, selects a result and excludes other mapped models', () => { + const window = new Window(); + const document = window.document; + const container = document.createElement('div'); + document.body.append(container); + const source = fs.readFileSync(new URL('../web/resolver/views/options_methods.js', import.meta.url), 'utf8'); + const block = source.slice(source.indexOf(' const getBaseModelMappingChoices ='), source.indexOf(' const renderTemplatePreview =')); + const dialog = { + baseModels: { base_models: [{ name: 'SDXL' }, { name: 'Pony' }, { name: 'Flux.1' }] }, + escapeHtml: value => value, + showDropdownList: list => { list.style.display = 'block'; }, + hideDropdownList: list => { list.style.display = 'none'; }, + enableWheelScrollChaining() {}, bindDropdownOutsideDismiss() {}, bindTooltips() {}, + }; + const methods = new Function('document', 'tokens', 'baseModelMappingsContainer', 'setStatus', 'syncAllTemplateControls', + `${block}; return { addBaseModelMappingRow, collectBaseModelPathMappings };` + ).call(dialog, document, { base_model_path_mappings: {} }, container, () => {}, () => {}); + methods.addBaseModelMappingRow('SDXL', 'SDXL'); + methods.addBaseModelMappingRow('', 'Pony-folder'); + const input = container.querySelectorAll('.mr-options-mapping-base')[1]; + const list = input.nextElementSibling; + input.dispatchEvent(new window.Event('focus')); + assert.equal(list.textContent.includes('SDXL'), false); + input.value = 'pon'; + input.dispatchEvent(new window.Event('input')); + assert.equal(list.textContent, 'Pony'); + list.firstElementChild.dispatchEvent(new window.MouseEvent('mousedown', { bubbles: true })); + assert.equal(input.value, 'Pony'); + assert.equal(list.style.display, 'none'); + assert.deepEqual(methods.collectBaseModelPathMappings(), { SDXL: 'SDXL', Pony: 'Pony-folder' }); + input.value = 'flux'; + input.dispatchEvent(new window.Event('input')); + input.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Enter' })); + assert.equal(input.value, 'Flux.1'); + input.dispatchEvent(new window.Event('focus')); + input.dispatchEvent(new window.KeyboardEvent('keydown', { key: 'Escape' })); + assert.equal(list.style.display, 'none'); + window.happyDOM.abort(); +}); diff --git a/web/resolver/views/options_methods.js b/web/resolver/views/options_methods.js index 78ef458..4f35539 100644 --- a/web/resolver/views/options_methods.js +++ b/web/resolver/views/options_methods.js @@ -2865,34 +2865,20 @@ export const optionsMethods = { }, {}); }; - const renderBaseModelMappingOptions = (currentValue = '', currentRow = null) => { + const getAvailableBaseModelMappingChoices = (currentValue = '', currentRow = null) => { const usedValues = new Set( getBaseModelMappingRows() .filter(row => row !== currentRow) .map(row => row.querySelector('.mr-options-mapping-base')?.value?.trim() || '') .filter(Boolean) ); - const options = [ - `` - ]; const choices = getBaseModelMappingChoices(); - if (currentValue && !choices.includes(currentValue)) { - options.push(``); - } - choices.forEach((name) => { - if (usedValues.has(name) && name !== currentValue) return; - options.push(``); - }); - return options.join(''); + return choices.filter(name => !usedValues.has(name) || name === currentValue); }; const refreshBaseModelMappingOptions = () => { getBaseModelMappingRows().forEach((row) => { - const select = row.querySelector('.mr-options-mapping-base'); - if (!select) return; - const currentValue = select.value; - select.innerHTML = renderBaseModelMappingOptions(currentValue, row); - select.value = currentValue; + row.refreshMappingChoices?.(); }); }; @@ -2903,14 +2889,16 @@ export const optionsMethods = { const row = document.createElement('div'); row.className = 'mr-options-mapping-row'; row.innerHTML = ` - +
+ +
+
`; const baseSelect = row.querySelector('.mr-options-mapping-base'); + const baseList = row.querySelector('.mr-download-target-list'); const pathInput = row.querySelector('.mr-options-mapping-path'); const removeBtn = row.querySelector('.mr-options-mapping-remove'); const markChanged = () => { @@ -2919,6 +2907,54 @@ export const optionsMethods = { syncAllTemplateControls(); }; + let filterText = ''; + const renderChoices = (filter = '', show = true) => { + filterText = filter; + const search = filter.trim().toLowerCase(); + const choices = getAvailableBaseModelMappingChoices(baseSelect.value.trim(), row) + .filter(name => name.toLowerCase().includes(search)); + baseList.innerHTML = choices.map(name => ( + `
${this.escapeHtml(name)}
` + )).join(''); + baseList.querySelectorAll('.mr-download-target-option').forEach(option => { + option.addEventListener('mousedown', event => { + event.preventDefault(); + baseSelect.value = decodeURIComponent(option.dataset.value); + this.hideDropdownList(baseList); + markChanged(); + }); + }); + if (show && choices.length) this.showDropdownList(baseList, baseSelect); + else if (!choices.length) this.hideDropdownList(baseList); + }; + row.refreshMappingChoices = () => { + if (baseList.style.display !== 'none') renderChoices(filterText, false); + }; + this.enableWheelScrollChaining(baseList); + this.bindDropdownOutsideDismiss(baseList, [baseSelect], () => this.hideDropdownList(baseList)); + baseSelect.addEventListener('focus', () => renderChoices()); + baseSelect.addEventListener('click', () => renderChoices()); + baseSelect.addEventListener('input', () => { + markChanged(); + renderChoices(baseSelect.value); + }); + baseSelect.addEventListener('keydown', event => { + if (event.key === 'Escape') this.hideDropdownList(baseList); + if (event.key === 'ArrowDown') { + event.preventDefault(); + renderChoices(baseSelect.value); + } + if (event.key === 'Enter') { + event.preventDefault(); + const option = baseList.querySelector('.mr-download-target-option'); + if (option && baseList.style.display !== 'none') { + baseSelect.value = decodeURIComponent(option.dataset.value); + markChanged(); + } + this.hideDropdownList(baseList); + } + }); + baseSelect?.addEventListener('change', markChanged); pathInput?.addEventListener('input', markChanged); pathInput?.addEventListener('keydown', (event) => { @@ -2928,6 +2964,7 @@ export const optionsMethods = { } }); removeBtn?.addEventListener('click', () => { + baseList._mlTreePicker?.cleanup(); row.remove(); if (!getBaseModelMappingRows().length) { addBaseModelMappingRow('', ''); @@ -2944,6 +2981,7 @@ export const optionsMethods = { const renderBaseModelMappingRows = (mappings = {}) => { if (!baseModelMappingsContainer) return; + getBaseModelMappingRows().forEach(row => row.querySelector('.mr-download-target-list')?._mlTreePicker?.cleanup()); baseModelMappingsContainer.innerHTML = ''; const entries = Object.entries(mappings || {}); if (!entries.length) {