fix(frontend): preserve local match rows during refresh

This commit is contained in:
Dariusz L
2026-08-04 19:40:04 +02:00
parent 5906523d0c
commit 058832073d
3 changed files with 173 additions and 13 deletions
@@ -5182,6 +5182,29 @@ test('local match status labels hash matches distinctly from exact matches', ()
);
});
test('local match refreshes preserve keyed rows and button handlers', () => {
const renderLocalMatchesContent = extractMethod(searchPanelMethodsSource, 'renderLocalMatchesContent');
const patchLocalMatchesContainer = extractMethod(
resolveDownloadMethodsSource,
'patchLocalMatchesContainer'
);
const refreshLocalMatchesUiForMissing = extractMethod(
resolveDownloadMethodsSource,
'refreshLocalMatchesUiForMissing'
);
const wireLocalMatchButtons = extractMethod(searchPanelMethodsSource, 'wireLocalMatchButtons');
assert.match(renderLocalMatchesContent, /data-local-match-key/);
assert.match(renderLocalMatchesContent, /encodeURIComponent/);
assert.match(patchLocalMatchesContainer, /local-match-key|localMatchKey/);
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.doesNotMatch(wireLocalMatchButtons, /addEventListener\('click'/);
});
test('local match status group warns when match folder is unsupported by node', () => {
const renderLocalMatchStatus = eval(`(${extractMethod(searchPanelMethodsSource, 'renderLocalMatchStatus')})`);
const getLocalMatchCategory = eval(`(${extractMethod(searchPanelMethodsSource, 'getLocalMatchCategory')})`);
@@ -1367,6 +1367,116 @@ export const resolveDownloadMethods = {
this.renderDownloadSnapshot(snapshot.downloadId, snapshot);
},
patchLocalMatchesContainer(container, renderedHtml = '') {
if (!container) return;
const documentRef = container.ownerDocument || globalThis.document;
if (!documentRef?.createElement) {
container.innerHTML = renderedHtml;
return;
}
const template = documentRef.createElement('template');
template.innerHTML = renderedHtml || '';
const nextRows = Array.from(template.content.querySelectorAll('.mr-match-row[data-local-match-key]'));
const currentRows = Array.from(container.querySelectorAll('.mr-match-row[data-local-match-key]'));
if (
!nextRows.length
|| !currentRows.length
|| currentRows.some(row => !row.dataset.localMatchKey)
) {
if (container.innerHTML !== renderedHtml) {
container.innerHTML = renderedHtml;
}
return;
}
const currentGroups = Array.from(container.querySelectorAll('.mr-stack-sm'));
const currentGroupsById = new Map(currentGroups.map(group => [group.id, group]));
const nextGroups = Array.from(template.content.querySelectorAll('.mr-stack-sm'));
const nextGroupsById = new Map();
const retainedGroups = new Set();
for (const nextGroup of nextGroups) {
const groupId = nextGroup.id || '';
let currentGroup = currentGroupsById.get(groupId);
if (!currentGroup) {
currentGroup = nextGroup.cloneNode(true);
currentGroup.querySelectorAll('.mr-match-row').forEach(row => row.remove());
}
nextGroupsById.set(groupId, currentGroup);
retainedGroups.add(currentGroup);
}
const currentRowsByKey = new Map(
currentRows.map(row => [row.dataset.localMatchKey, row])
);
const retainedRows = new Set();
const nextRowsByKey = new Map();
for (const nextRow of nextRows) {
const key = nextRow.dataset.localMatchKey;
let currentRow = currentRowsByKey.get(key);
if (!currentRow) {
currentRow = nextRow.cloneNode(true);
} else {
this.syncSearchElementAttributes(currentRow, nextRow);
if (currentRow.innerHTML !== nextRow.innerHTML) {
currentRow.innerHTML = nextRow.innerHTML;
}
}
const nextGroup = nextRow.closest('.mr-stack-sm');
const targetParent = nextGroup
? nextGroupsById.get(nextGroup.id || '')
: container;
if (!targetParent) return;
targetParent.appendChild(currentRow);
retainedRows.add(currentRow);
nextRowsByKey.set(key, currentRow);
}
for (const currentRow of currentRows) {
if (!retainedRows.has(currentRow)) currentRow.remove();
}
for (const currentGroup of currentGroups) {
if (!retainedGroups.has(currentGroup)) currentGroup.remove();
}
const nextToggle = template.content.querySelector('.mr-local-alternatives-toggle');
const currentToggle = container.querySelector('.mr-local-alternatives-toggle');
let nextDomToggle = null;
if (nextToggle) {
nextDomToggle = currentToggle || nextToggle.cloneNode(true);
if (currentToggle) {
this.syncSearchElementAttributes(currentToggle, nextToggle);
}
} else if (currentToggle) {
currentToggle.remove();
}
const retainedChildren = new Set();
const nextDomChildren = [];
for (const nextChild of Array.from(template.content.children)) {
let nextDomChild = null;
if (nextChild.classList.contains('mr-match-row')) {
nextDomChild = nextRowsByKey.get(nextChild.dataset.localMatchKey);
} else if (nextChild.classList.contains('mr-local-alternatives-toggle')) {
nextDomChild = nextDomToggle;
} else if (nextChild.classList.contains('mr-stack-sm')) {
nextDomChild = nextGroupsById.get(nextChild.id || '');
}
if (!nextDomChild) continue;
retainedChildren.add(nextDomChild);
nextDomChildren.push(nextDomChild);
}
Array.from(container.children).forEach(child => {
if (!retainedChildren.has(child)) child.remove();
});
nextDomChildren.forEach(child => container.appendChild(child));
},
async refreshLocalMatchesForDownloadedMissing(missing, downloadedFilename, { progressDiv = null, category = '', downloadPath = '', downloadDirectory = '' } = {}) {
if (!missing) return [];
@@ -1383,7 +1493,10 @@ export const resolveDownloadMethods = {
}
const body = this.contentElement?.querySelector(`#local-matches-body-${this.getMissingModelDomKey(missing)}`);
if (body) {
body.innerHTML = `<div class="mr-no-matches">Checking local matches for ${this.escapeHtml(targetFilename)}...</div>`;
this.patchLocalMatchesContainer(
body,
`<div class="mr-no-matches">Checking local matches for ${this.escapeHtml(targetFilename)}...</div>`
);
}
const data = await this.fetchLocalMatches(targetFilename, category || missing.category || '', true);
@@ -1410,7 +1523,10 @@ export const resolveDownloadMethods = {
const body = this.contentElement.querySelector(`#local-matches-body-${this.getMissingModelDomKey(missing)}`);
const displayIndex = Number.isFinite(missing.__displayIndex) ? missing.__displayIndex : 0;
if (body) {
body.innerHTML = this.renderLocalMatchesContent(missing, displayIndex);
this.patchLocalMatchesContainer(
body,
this.renderLocalMatchesContent(missing, displayIndex)
);
this.wireLocalMatchButtons(body, missing, displayIndex);
}
@@ -1450,7 +1566,10 @@ export const resolveDownloadMethods = {
button.classList.add('mr-btn-is-disabled', 'mr-is-refreshing');
}
if (body) {
body.innerHTML = `<div class="mr-no-matches">Refreshing local matches for ${this.escapeHtml(displayName)}...</div>`;
this.patchLocalMatchesContainer(
body,
`<div class="mr-no-matches">Refreshing local matches for ${this.escapeHtml(displayName)}...</div>`
);
}
const data = await this.fetchLocalMatches(targetFilename, missing.category || '', true);
+28 -10
View File
@@ -2319,9 +2319,14 @@ export const searchPanelMethods = {
const identityAttr = localMatchIdentity
? ` data-local-match-identity="${this.escapeHtml(localMatchIdentity)}"`
: '';
const localMatchKey = localMatchIdentity
|| matchPath
|| match.filename
|| `match-${matchIndex}`;
const localMatchKeyAttr = ` data-local-match-key="${this.escapeHtml(encodeURIComponent(String(localMatchKey)))}"`;
const rowClass = `mr-match-row${isBestMatch ? ' mr-best-match' : ''}`;
html += `<div class="${rowClass}"${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
html += `<div class="${rowClass}"${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);
@@ -2354,7 +2359,12 @@ export const searchPanelMethods = {
const identityAttr = localMatchIdentity
? ` data-local-match-identity="${this.escapeHtml(localMatchIdentity)}"`
: '';
html += `<div class="mr-match-row"${identityAttr}${this.getContextMenuAttrs(contextModel)}>`;
const localMatchKey = localMatchIdentity
|| matchPath
|| 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 += 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);
@@ -2393,10 +2403,9 @@ export const searchPanelMethods = {
const buttonId = `resolve-${missingIndex}-${missing.node_id}-${missing.widget_index}-${matchIndex}`;
const resolveButton = container.querySelector(`#${buttonId}`);
if (resolveButton) {
resolveButton.onclick = null;
resolveButton.addEventListener('click', () => {
resolveButton.onclick = () => {
this.queueResolution(missing, match.model);
});
};
}
});
@@ -2406,9 +2415,9 @@ export const searchPanelMethods = {
const altBtnId = `resolve-alt-${missingIndex}-${missing.node_id}-${missing.widget_index}-${mIdx}`;
const altBtn = container.querySelector(`#${altBtnId}`);
if (altBtn) {
altBtn.addEventListener('click', () => {
altBtn.onclick = () => {
this.queueResolution(missing, match.model);
});
};
}
}
}
@@ -2654,20 +2663,29 @@ export const searchPanelMethods = {
const bodyId = `local-matches-body-${this.getMissingModelDomKey(missing)}`;
const container = this.contentElement.querySelector(`#${bodyId}`);
if (container) {
container.innerHTML = `<div class="mr-no-matches">Searching local matches for "${missing.civitai_info.expected_filename}"...</div>`;
this.patchLocalMatchesContainer(
container,
`<div class="mr-no-matches">Searching local matches for "${this.escapeHtml(missing.civitai_info.expected_filename)}"...</div>`
);
}
try {
await this.fetchUrnLocalMatches(missing);
if (container) {
container.innerHTML = this.renderLocalMatchesContent(missing, missing.__displayIndex || 0);
this.patchLocalMatchesContainer(
container,
this.renderLocalMatchesContent(missing, missing.__displayIndex || 0)
);
this.wireLocalMatchButtons(this.contentElement, missing, missing.__displayIndex || 0);
}
this.refreshMissingListRow(missing, { refreshBaseModels: true });
} catch (error) {
console.error('Model Resolver: URN local match refresh error:', error);
if (container) {
container.innerHTML = `<div class="mr-no-matches">Failed to refresh local matches.</div>`;
this.patchLocalMatchesContainer(
container,
'<div class="mr-no-matches">Failed to refresh local matches.</div>'
);
}
}
},