perf(frontend): delegate dynamic action clicks
This commit is contained in:
@@ -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 () => {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user