refactor: preserve downloads panel DOM state during active refresh

Patch the panel element instead of replacing innerHTML, maintaining hover and tooltip state for active download cards.
This commit is contained in:
Dariusz L
2026-07-19 15:10:12 +02:00
parent db2c77290c
commit e7ee26cad8
2 changed files with 115 additions and 17 deletions
+25 -15
View File
@@ -608,14 +608,8 @@ test('downloads tab shows active downloads from all workflow tabs', () => {
});
test('downloads tab renders workflow label for active downloads', () => {
const renderQueueDownloads = eval(`(${extractMethod(queueMethodsSource, 'renderQueueDownloads')})`);
const renderQueueDownloadsHtml = eval(`(${extractMethod(queueMethodsSource, 'renderQueueDownloadsHtml')})`);
const dialog = {
queueList: {
innerHTML: '',
querySelectorAll() {
return [];
},
},
escapeHtml(value) {
return String(value).replace(/[&<>\"]/g, char => ({ '&': '&amp;', '<': '&lt;', '>': '&gt;', '"': '&quot;' }[char]));
},
@@ -631,15 +625,23 @@ test('downloads tab renders workflow label for active downloads', () => {
getDownloadFolderContext() {
return null;
},
renderQueueDownloadsHtml(downloads) {
const info = downloads[0]?.info || {};
const workflowLabel = this.getDownloadWorkflowLabel(info);
return `<span>Workflow</span> ${workflowLabel}`;
getDownloadDisplayProgress(progress) {
return {
percent: progress.progress || 0,
downloaded: progress.downloaded || 0,
totalSize: progress.total_size || 0,
isFinalizing: false,
};
},
formatDownloadProgressMeta() {
return '';
},
getDownloadQueueContext() {
return null;
},
wireDownloadsPanelControls() {},
};
renderQueueDownloads.call(dialog, [{
const html = renderQueueDownloadsHtml.call(dialog, [{
downloadId: 'download-1',
info: {
workflowLabel: 'Workflow B',
@@ -648,8 +650,16 @@ test('downloads tab renders workflow label for active downloads', () => {
},
}]);
assert.match(dialog.queueList.innerHTML, /<span>Workflow<\/span>/);
assert.match(dialog.queueList.innerHTML, /Workflow B/);
assert.match(html, /<span>Workflow<\/span>/);
assert.match(html, /Workflow B/);
});
test('active download refresh preserves existing cards for hover and tooltip state', () => {
const renderQueueDownloadsHtml = extractMethod(queueMethodsSource, 'renderQueueDownloadsHtml');
assert.match(queueMethodsSource, /patchDownloadsPanelElement\(currentPanel, nextPanel\)/);
assert.match(renderQueueDownloadsHtml, /data-download-id=/);
assert.match(queueMethodsSource, /patchDownloadsPanelElement\(currentElement, nextElement\)/);
});
test('active workflow label prefers selected ComfyUI workflow tab name over route hash', () => {
+90 -2
View File
@@ -1161,7 +1161,7 @@ export const queueMethods = {
renderDownloadsPanel(downloads = [], history = this.getDownloadHistory()) {
const activeSubTab = this.queueDownloadsActiveTab === 'history' ? 'history' : 'active';
const activeSelected = activeSubTab === 'active';
let html = '<div class="mr-downloads-panel">';
let html = `<div class="mr-downloads-panel" data-downloads-view="${activeSubTab}">`;
html += '<div class="mr-tabs mr-queue-tabs mr-downloads-subtabs" role="tablist" aria-label="Downloads views">';
html += `<button type="button" class="mr-tab mr-queue-tab mr-downloads-subtab${activeSelected ? ' mr-tab-active' : ''}" data-downloads-tab="active" aria-selected="${activeSelected ? 'true' : 'false'}"><span class="mr-tab-label">Active (${downloads.length})</span></button>`;
html += `<button type="button" class="mr-tab mr-queue-tab mr-downloads-subtab${!activeSelected ? ' mr-tab-active' : ''}" data-downloads-tab="history" aria-selected="${!activeSelected ? 'true' : 'false'}"><span class="mr-tab-label">History (${history.length})</span></button>`;
@@ -1171,10 +1171,98 @@ export const queueMethods = {
: this.renderDownloadHistoryHtml(history);
html += '</div>';
const currentPanel = this.queueList.querySelector?.(`.mr-downloads-panel[data-downloads-view="${activeSubTab}"]`);
if (currentPanel && typeof document !== 'undefined') {
const template = document.createElement('template');
template.innerHTML = html;
const nextPanel = template.content.firstElementChild;
if (nextPanel) {
this.patchDownloadsPanelElement(currentPanel, nextPanel);
this.wireDownloadsPanelControls();
return;
}
}
this.queueList.innerHTML = html;
this.wireDownloadsPanelControls();
},
patchDownloadsPanelElement(currentElement, nextElement) {
const syncAttributes = (current, next) => {
const nextAttributeNames = new Set(Array.from(next.attributes || [], attribute => attribute.name));
for (const attribute of Array.from(next.attributes || [])) {
if (current.getAttribute(attribute.name) !== attribute.value) {
current.setAttribute(attribute.name, attribute.value);
}
}
for (const attribute of Array.from(current.attributes || [])) {
if (!nextAttributeNames.has(attribute.name)) {
current.removeAttribute(attribute.name);
}
}
};
const getNodeKey = (node) => {
if (node?.nodeType !== 1) return '';
return node.getAttribute?.('data-download-id') || '';
};
const canPatchNode = (current, next) => {
if (!current || current.nodeType !== next.nodeType) return false;
if (current.nodeType !== 1) return true;
if (current.tagName !== next.tagName) return false;
if ((current.getAttribute('class') || '') !== (next.getAttribute('class') || '')) return false;
const currentKey = getNodeKey(current);
const nextKey = getNodeKey(next);
return !currentKey && !nextKey ? true : currentKey === nextKey;
};
const patchNode = (current, next) => {
if (!canPatchNode(current, next)) {
const replacement = next.cloneNode(true);
current.replaceWith(replacement);
return replacement;
}
if (current.nodeType !== 1) {
if (current.nodeValue !== next.nodeValue) current.nodeValue = next.nodeValue;
return current;
}
syncAttributes(current, next);
patchChildren(current, next);
return current;
};
const patchChildren = (current, next) => {
const nextChildren = Array.from(next.childNodes || []);
let cursor = current.firstChild;
for (const nextChild of nextChildren) {
const nextKey = getNodeKey(nextChild);
let currentChild = cursor;
if (nextKey) {
currentChild = Array.from(current.childNodes || []).find(child => getNodeKey(child) === nextKey) || null;
if (currentChild && currentChild !== cursor) {
current.insertBefore(currentChild, cursor);
}
}
if (!currentChild) {
current.appendChild(nextChild.cloneNode(true));
cursor = null;
continue;
}
const patchedChild = patchNode(currentChild, nextChild);
cursor = patchedChild.nextSibling;
}
while (cursor) {
const nextSibling = cursor.nextSibling;
cursor.remove();
cursor = nextSibling;
}
};
patchNode(currentElement, nextElement);
},
wireDownloadsPanelControls() {
const bindInstantAction = (button, handler) => {
if (!button || button._hasListener) return;
@@ -1285,7 +1373,7 @@ export const queueMethods = {
? this.getContextMenuAttrs(contextModel, contextTooltip)
: '';
html += `<div class="mr-queue-item mr-download-queue-item"${contextData}>`;
html += `<div class="mr-queue-item mr-download-queue-item" data-download-id="${this.escapeHtml(downloadId)}"${contextData}>`;
html += `<div class="mr-queue-item-title mr-download-queue-title">`;
html += `<span data-tooltip="${this.escapeHtml(filename)}">${this.escapeHtml(filename)}</span>`;
html += `<span class="mr-download-queue-status">${this.escapeHtml(statusLabel)}</span>`;