fix(frontend): preserve local match rows during refresh
This commit is contained in:
@@ -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);
|
||||
|
||||
@@ -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>'
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
Reference in New Issue
Block a user