perf(frontend): delegate dynamic action clicks

This commit is contained in:
Dariusz L
2026-08-04 20:20:42 +02:00
parent d2c4d513db
commit adc29077c8
4 changed files with 92 additions and 72 deletions
+41 -1
View File
@@ -60,6 +60,7 @@ const patchLocalMatchesContainer = eval(`(${extractMethod(resolveDownloadMethods
const resolveUrnAsync = eval(`(${extractMethod(resolveDownloadMethodsSource, 'resolveUrnAsync')})`);
const fetchUrnLocalMatches = eval(`(${extractMethod(searchPanelMethodsSource, 'fetchUrnLocalMatches')})`);
const scheduleSearchUiRefresh = eval(`(${extractMethod(searchPanelMethodsSource, 'scheduleSearchUiRefresh')})`);
const wireLocalMatchButtons = eval(`(${extractMethod(searchPanelMethodsSource, 'wireLocalMatchButtons')})`);
const patchQueuedSelections = eval(`(${extractMethod(queueMethodsSource, 'patchQueuedSelections')})`);
const patchDownloadsPanelElement = eval(`(${extractMethod(queueMethodsSource, 'patchDownloadsPanelElement')})`);
const patchLoadedModelsProgress = eval(`(${extractMethod(renderFormatMethodsSource, 'patchLoadedModelsProgress')})`);
@@ -253,15 +254,54 @@ test('search download button wiring remains idempotent on the same node', () =>
const window = new Window();
const dialog = createDialog(window);
const container = window.document.createElement('div');
container.innerHTML = '<button class="search-download-btn" data-url="https://example.test/model" data-filename="model.safetensors" data-category="checkpoints"></button>';
container.innerHTML = `
<button class="search-download-btn" data-url="https://example.test/model" data-filename="model.safetensors" data-category="checkpoints"></button>
<button class="search-show-details-btn" data-model="%7B%22name%22%3A%22Example%22%7D"></button>
`;
let calls = 0;
let details = null;
dialog.downloadFromSearch = () => { calls += 1; };
dialog.showSourceModelDetails = model => { details = model; };
dialog.wireSearchDownloadButtons(container, {});
dialog.wireSearchDownloadButtons(container, {});
container.querySelector('.search-download-btn').click();
container.querySelector('.search-show-details-btn').click();
assert.equal(calls, 1);
assert.deepEqual(details, { name: 'Example' });
});
test('local match action delegation remains idempotent across keyed rows', () => {
const window = new Window();
const container = window.document.createElement('div');
container.innerHTML = `
<div class="mr-match-row" data-local-match-index="0">
<button class="mr-local-link-btn" type="button">Best</button>
</div>
<div class="mr-match-row" data-local-match-index="0" data-local-match-alternative="true">
<button class="mr-local-link-btn" type="button">Alternative</button>
</div>
`;
const missing = {
matches: [
{ confidence: 100, model: { id: 'best' } },
{ confidence: 80, model: { id: 'alternative' } },
],
};
const queued = [];
const dialog = {
queueResolution(_missing, model) {
queued.push(model.id);
},
};
wireLocalMatchButtons.call(dialog, container, missing);
wireLocalMatchButtons.call(dialog, container, missing);
container.querySelector('.mr-match-row button').click();
container.querySelector('[data-local-match-alternative="true"] button').click();
assert.deepEqual(queued, ['best', 'alternative']);
});
test('stale URN responses cannot overwrite the latest UI request', async () => {
+8 -5
View File
@@ -5232,8 +5232,9 @@ test('local match refreshes preserve keyed rows and button handlers', () => {
assert.match(patchLocalMatchesContainer, /mr-local-alternatives-toggle/);
assert.match(refreshLocalMatchesUiForMissing, /patchLocalMatchesContainer/);
assert.doesNotMatch(refreshLocalMatchesUiForMissing, /body\.innerHTML\s*=/);
assert.match(wireLocalMatchButtons, /resolveButton\.onclick\s*=/);
assert.match(wireLocalMatchButtons, /altBtn\.onclick\s*=/);
assert.match(wireLocalMatchButtons, /bindEventOnce/);
assert.match(wireLocalMatchButtons, /local-match-actions/);
assert.doesNotMatch(wireLocalMatchButtons, /\.onclick\s*=/);
assert.doesNotMatch(wireLocalMatchButtons, /addEventListener\('click'/);
});
@@ -5843,9 +5844,11 @@ test('search result refreshes preserve keyed progress and result DOM', () => {
assert.doesNotMatch(displaySearchResults, /container\.innerHTML\s*=/);
assert.match(patchSearchProgressElement, /data-search-progress-source|searchProgressSource/);
assert.match(patchSearchResultsTable, /data-search-result-key|searchResultKey/);
assert.match(wireSearchDownloadButtons, /mlSearchDownloadBound/);
assert.match(wireSearchDownloadButtons, /mlSearchOpenPageBound/);
assert.match(wireSearchDownloadButtons, /mlSearchDetailsBound/);
assert.match(wireSearchDownloadButtons, /bindEventOnce/);
assert.match(wireSearchDownloadButtons, /search-result-actions/);
assert.doesNotMatch(wireSearchDownloadButtons, /mlSearchDownloadBound/);
assert.doesNotMatch(wireSearchDownloadButtons, /mlSearchOpenPageBound/);
assert.doesNotMatch(wireSearchDownloadButtons, /mlSearchDetailsBound/);
assert.match(resolveDownloadMethodsSource, /rows\.push\(\{ \.\.\.row, __searchResultKey: rowKey \}\)/);
assert.match(searchPanelMethodsSource, /data-search-progress-source=/);
assert.match(searchPanelMethodsSource, /data-search-result-key=/);
@@ -4,7 +4,7 @@ import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js"
import { getCivitaiModelUrl } from "../globals.js";
import { normalizeSha256 } from "../utils/hash_utils.js";
import { normalizePathIdentity } from "../utils/html_utils.js";
import { syncElementAttributes } from "../utils/dom_patch_utils.js";
import { bindEventOnce, syncElementAttributes } from "../utils/dom_patch_utils.js";
const log = createModuleLogger('resolve_download_methods');
export const resolveDownloadMethods = {
@@ -2996,12 +2996,13 @@ export const resolveDownloadMethods = {
if (!container) return;
this.wireSearchHashMatchHighlights?.(container);
bindEventOnce(container, 'click', (event) => {
const btn = event.target?.closest?.(
'.search-download-btn, .search-open-page-btn, .search-show-details-btn'
);
if (!btn || !container.contains(btn)) return;
const downloadBtns = container.querySelectorAll('.search-download-btn');
downloadBtns.forEach(btn => {
if (btn.dataset.mlSearchDownloadBound === 'true') return;
btn.dataset.mlSearchDownloadBound = 'true';
btn.addEventListener('click', () => {
if (btn.classList.contains('search-download-btn')) {
const url = btn.dataset.url;
const filename = btn.dataset.filename;
const category = btn.dataset.category;
@@ -3022,14 +3023,10 @@ export const resolveDownloadMethods = {
downloadMetadata = null;
}
this.downloadFromSearch(missing, url, filename, category, btn, pathMetadata, downloadMetadata);
});
});
return;
}
const openPageBtns = container.querySelectorAll('.search-open-page-btn');
openPageBtns.forEach(btn => {
if (btn.dataset.mlSearchOpenPageBound === 'true') return;
btn.dataset.mlSearchOpenPageBound = 'true';
btn.addEventListener('click', () => {
if (btn.classList.contains('search-open-page-btn')) {
const url = btn.dataset.url;
if (!url) return;
@@ -3037,23 +3034,17 @@ export const resolveDownloadMethods = {
if (opened) {
opened.opener = null;
}
});
});
return;
}
const detailsBtns = container.querySelectorAll('.search-show-details-btn');
detailsBtns.forEach(btn => {
if (btn.dataset.mlSearchDetailsBound === 'true') return;
btn.dataset.mlSearchDetailsBound = 'true';
btn.addEventListener('click', () => {
try {
const model = JSON.parse(decodeURIComponent(btn.dataset.model || ''));
this.showSourceModelDetails(model);
} catch (error) {
console.error('Model Resolver: failed to open model details:', error);
this.showNotification?.('Failed to open model details.', 'error');
}
});
});
try {
const model = JSON.parse(decodeURIComponent(btn.dataset.model || ''));
this.showSourceModelDetails(model);
} catch (error) {
console.error('Model Resolver: failed to open model details:', error);
this.showNotification?.('Failed to open model details.', 'error');
}
}, 'search-result-actions');
},
wireDownloadSearchPanel(container, missing) {
+23 -37
View File
@@ -2,6 +2,7 @@ import { getSvgIcon } from "../../utils/icon_utils.js";
import { getModelCardUrl, parseHuggingFaceFileUrl } from "../utils/url_utils.js";
import { getCivitaiModelUrl } from "../globals.js";
import { safeStorage, normalizePathIdentity } from "../utils/html_utils.js";
import { bindEventOnce } from "../utils/dom_patch_utils.js";
const localStorage = safeStorage;
export const searchPanelMethods = {
isAutoFillBaseModelEnabled() {
@@ -2360,7 +2361,7 @@ export const searchPanelMethods = {
const localMatchKeyAttr = ` data-local-match-key="${this.escapeHtml(encodeURIComponent(String(localMatchKey)))}"`;
const rowClass = `mr-match-row${isBestMatch ? ' mr-best-match' : ''}`;
html += `<div class="${rowClass}"${localMatchKeyAttr}${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
html += `<div class="${rowClass}" data-local-match-index="${matchIndex}"${localMatchKeyAttr}${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
html += this.getConfidenceBadge(match.confidence);
html += `<span class="mr-match-filename"${this.getModelPreviewTooltipAttrs(match.model || match, matchPath)}>${this.escapeHtml(matchPath)}</span>`;
html += this.renderLocalMatchStatusGroup(missing, match, hashLabelMap);
@@ -2398,7 +2399,7 @@ export const searchPanelMethods = {
|| match.filename
|| `alternative-${mIdx}`;
const localMatchKeyAttr = ` data-local-match-key="${this.escapeHtml(encodeURIComponent(String(localMatchKey)))}"`;
html += `<div class="mr-match-row"${localMatchKeyAttr}${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
html += `<div class="mr-match-row" data-local-match-index="${mIdx}" data-local-match-alternative="true"${localMatchKeyAttr}${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
html += this.getConfidenceBadge(match.confidence);
html += `<span class="mr-match-filename"${this.getModelPreviewTooltipAttrs(match.model || match, matchPath)}>${this.escapeHtml(matchPath)}</span>`;
html += this.renderLocalMatchStatusGroup(missing, match, hashLabelMap);
@@ -2418,43 +2419,28 @@ export const searchPanelMethods = {
return html;
},
wireLocalMatchButtons(container, missing, missingIndex = 0) {
const allMatches = missing.matches || [];
const filteredMatches = allMatches.filter(m => m.confidence >= 70);
const perfectMatches = filteredMatches.filter(m => m.confidence === 100);
const otherMatches = filteredMatches.filter(m => m.confidence < 100 && m.confidence >= 70);
const matchesToShow = perfectMatches.length > 0
? perfectMatches
: otherMatches.sort((a, b) => b.confidence - a.confidence).slice(0, 5);
wireLocalMatchButtons(container, missing) {
bindEventOnce(container, 'click', (event) => {
const button = event.target?.closest?.('.mr-local-link-btn');
if (!button || !container.contains(button)) return;
const sortedMatches = matchesToShow.sort((a, b) => {
if (a.confidence === 100 && b.confidence !== 100) return -1;
if (a.confidence !== 100 && b.confidence === 100) return 1;
return b.confidence - a.confidence;
});
const row = button.closest('.mr-match-row');
const matchIndex = Number(row?.dataset.localMatchIndex);
if (!row || !Number.isInteger(matchIndex) || matchIndex < 0) return;
sortedMatches.forEach((match, matchIndex) => {
const buttonId = `resolve-${missingIndex}-${missing.node_id}-${missing.widget_index}-${matchIndex}`;
const resolveButton = container.querySelector(`#${buttonId}`);
if (resolveButton) {
resolveButton.onclick = () => {
this.queueResolution(missing, match.model);
};
const filteredMatches = (missing.matches || []).filter(match => match.confidence >= 70);
const perfectMatches = filteredMatches.filter(match => match.confidence === 100);
const otherMatches = filteredMatches.filter(match => match.confidence < 100 && match.confidence >= 70);
const visibleMatches = perfectMatches.length > 0
? perfectMatches
: otherMatches.sort((left, right) => right.confidence - left.confidence).slice(0, 5);
const match = row.dataset.localMatchAlternative === 'true'
? otherMatches[matchIndex]
: visibleMatches[matchIndex];
if (match?.model) {
this.queueResolution(missing, match.model);
}
});
if (otherMatches && otherMatches.length > 0) {
for (let mIdx = 0; mIdx < otherMatches.length; mIdx++) {
const match = otherMatches[mIdx];
const altBtnId = `resolve-alt-${missingIndex}-${missing.node_id}-${missing.widget_index}-${mIdx}`;
const altBtn = container.querySelector(`#${altBtnId}`);
if (altBtn) {
altBtn.onclick = () => {
this.queueResolution(missing, match.model);
};
}
}
}
}, 'local-match-actions');
this.wireLocalHashMatchResultHighlights?.(container);
this.bindTooltips?.(container);
@@ -2723,7 +2709,7 @@ export const searchPanelMethods = {
container,
this.renderLocalMatchesContent(missing, missing.__displayIndex || 0)
);
this.wireLocalMatchButtons(this.contentElement, missing, missing.__displayIndex || 0);
this.wireLocalMatchButtons(container, missing);
}
this.refreshMissingListRow(missing, { refreshBaseModels: true });
} catch (error) {