Add searchable base model path mapping picker
This commit is contained in:
@@ -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();
|
||||
});
|
||||
@@ -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 = [
|
||||
`<option value="">Select Base Model</option>`
|
||||
];
|
||||
const choices = getBaseModelMappingChoices();
|
||||
if (currentValue && !choices.includes(currentValue)) {
|
||||
options.push(`<option value="${this.escapeHtml(currentValue)}" selected>${this.escapeHtml(currentValue)}</option>`);
|
||||
}
|
||||
choices.forEach((name) => {
|
||||
if (usedValues.has(name) && name !== currentValue) return;
|
||||
options.push(`<option value="${this.escapeHtml(name)}" ${name === currentValue ? 'selected' : ''}>${this.escapeHtml(name)}</option>`);
|
||||
});
|
||||
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 = `
|
||||
<select class="mr-options-input mr-options-mapping-base">
|
||||
${renderBaseModelMappingOptions(baseModel, row)}
|
||||
</select>
|
||||
<div class="mr-download-target-wrap">
|
||||
<input class="mr-options-input mr-options-mapping-base" type="text" autocomplete="off" placeholder="Search Base Models…" value="${this.escapeHtml(baseModel)}">
|
||||
<div class="mr-download-target-list"></div>
|
||||
</div>
|
||||
<input class="mr-options-input mr-options-mapping-path" type="text" placeholder="Custom path (e.g., SDXL/Pony)" value="${this.escapeHtml(pathValue)}">
|
||||
<button type="button" class="mr-options-mapping-remove" aria-label="Remove mapping" data-tooltip="Remove mapping">×</button>
|
||||
`;
|
||||
|
||||
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 => (
|
||||
`<div class="mr-download-target-option" data-value="${encodeURIComponent(name)}">${this.escapeHtml(name)}</div>`
|
||||
)).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) {
|
||||
|
||||
Reference in New Issue
Block a user