fix: preserve user queue, selections, and detail state during workflow refreshes and prevent open reload flicker

This commit is contained in:
Dariusz L
2026-11-02 23:59:54 +01:00
parent b68bf311a4
commit f9d9fe65d6
10 changed files with 468 additions and 78 deletions
+108
View File
@@ -3388,6 +3388,114 @@ test('workflow synchronization carries searched results across a strength-only s
assert.equal(dialog.preserveSearchCacheAcrossNextWorkflowSync, false);
});
test('same route signature update carries forward pending resolutions and download target selections', () => {
const syncWorkflowScopedQueue = eval(
`(${extractMethod(workflowStateMethodsSource, 'syncWorkflowScopedQueue')})`
);
const workflow = { nodes: [{ id: 1, type: 'LoraLoader' }] };
const queuedItem = { missing_key: 'lora:test', target_path: 'E:/models/test.safetensors' };
const savedTarget = { category: 'loras', subfolder: 'anime' };
const pendingSelections = new Map([
['workflow-a\nold-signature', [queuedItem]],
]);
const targetSelections = new Map([
['workflow-a\nold-signature', new Map([['missing-1', savedTarget]])],
]);
let queueVisibilityUpdated = false;
const dialog = {
activeWorkflowRouteKey: 'workflow-a',
activeWorkflowSignature: 'old-signature',
activeMissingWorkflowSignature: 'old-missing-signature',
workflowPendingSelections: pendingSelections,
workflowDownloadTargetSelections: targetSelections,
pendingResolutions: [queuedItem],
downloadTargetSelections: new Map([['missing-1', savedTarget]]),
searchResultCache: new Map(),
getCurrentWorkflow: () => workflow,
getActiveWorkflowRouteKey: () => 'workflow-a',
getWorkflowSignature: () => 'new-signature',
getMissingWorkflowSignature: () => 'new-missing-signature',
getWorkflowScopedQueueKey(route = dialog.activeWorkflowRouteKey, signature = dialog.activeMissingWorkflowSignature || dialog.activeWorkflowSignature) {
return `${route}\n${signature}`;
},
cloneSearchResultCache(cache) { return new Map(cache); },
clonePendingResolutions(list) { return list.map(item => ({ ...item })); },
cloneDownloadTargetSelections(map) { return new Map(map); },
savePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.workflowPendingSelections.set(key, this.clonePendingResolutions(this.pendingResolutions));
},
saveAnalysisCacheForActiveWorkflow() {},
saveLoadedModelsCacheForActiveWorkflow() {},
saveSearchCacheForActiveWorkflow() {},
saveDownloadTargetSelectionsForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.workflowDownloadTargetSelections.set(key, this.cloneDownloadTargetSelections(this.downloadTargetSelections));
},
clearWorkflowScopedState() {
this.pendingResolutions = [];
this.downloadTargetSelections = new Map();
},
restorePendingQueueForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.pendingResolutions = this.workflowPendingSelections.get(key) || [];
},
restoreAnalysisCacheForActiveWorkflow() {},
restoreLoadedModelsCacheForActiveWorkflow() {},
restoreSearchCacheForActiveWorkflow() {},
restoreDownloadTargetSelectionsForActiveWorkflow() {
const key = this.getWorkflowScopedQueueKey();
this.downloadTargetSelections = this.workflowDownloadTargetSelections.get(key) || new Map();
},
rebuildPendingIndex() {},
updateApplyPendingButton() {},
updateQueuePanel() {},
updateQueueVisibility() { queueVisibilityUpdated = true; },
};
syncWorkflowScopedQueue.call(dialog, workflow);
assert.equal(dialog.pendingResolutions.length, 1);
assert.deepEqual(dialog.pendingResolutions[0], queuedItem);
assert.equal(dialog.downloadTargetSelections.get('missing-1'), savedTarget);
assert.equal(queueVisibilityUpdated, true);
});
test('refreshForActiveWorkflowChange preserves content for window-focus and visibility on the same route', async () => {
const log = { debug() {} };
const refreshForActiveWorkflowChange = eval(
`(${extractMethod(workflowStateMethodsSource, 'refreshForActiveWorkflowChange')})`
);
const workflow = { nodes: [{ id: 10, type: 'CheckpointLoaderSimple' }] };
let loadArguments = null;
const dialog = {
_workflowRefreshGeneration: 1,
activeWorkflowRouteKey: 'workflow-a',
activeWorkflowSignature: 'old-signature',
activeMissingWorkflowSignature: 'old-missing-signature',
activeTab: 'missing',
contentElement: { style: {} },
isVisible: () => true,
getActiveWorkflowRouteKey: () => 'workflow-a',
getCurrentWorkflow: () => workflow,
getWorkflowSignature: () => 'new-signature',
getMissingWorkflowSignature: () => 'new-missing-signature',
syncWorkflowScopedQueue() {},
async loadWorkflowData(...args) {
loadArguments = args;
},
};
await refreshForActiveWorkflowChange.call(dialog, {
reason: 'window-focus',
expectedRoute: 'workflow-a',
previousSignature: 'old-signature',
attempt: 8,
generation: 1,
candidateRoute: 'workflow-a',
candidateSignature: 'new-signature',
});
assert.equal(loadArguments[0], workflow);
assert.deepEqual(loadArguments[1], { preserveContent: true });
});
test('pending node context model resolves against the current workflow analysis before action', async () => {
const resolveNodeContextMenuModel = eval(`(${extractMethod(modelResolverSource, 'resolveNodeContextMenuModel')})`);
const pendingModel = {
+2 -1
View File
@@ -886,8 +886,9 @@ test('metadata transfer renders locate button for target nodes', () => {
assert.match(html, /data-image-transfer-locate/);
assert.match(html, /data-node-id="1"/);
assert.match(html, /data-tooltip="Center this node in the ComfyUI graph\."/);
assert.match(html, /aria-label="Center CLIPLoader in the ComfyUI graph"/);
assert.match(html, /aria-label="Center CLIPLoader · Node 1[^"]* in the ComfyUI graph"/);
assert.match(html, /class="mr-node-chip-icon"/);
assert.match(html, /class="mr-missing-row-node-label">CLIPLoader · Node 1/);
});
test('metadata transfer locate button triggers locateNodeInGraph and does not toggle node selection', () => {
+204
View File
@@ -0,0 +1,204 @@
import test from 'node:test';
import assert from 'node:assert/strict';
import fs from 'node:fs';
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { tabsLoadedMethods } from '../web/resolver/views/tabs_loaded_methods.js';
import { safeStorage } from '../web/resolver/utils/html_utils.js';
globalThis.safeStorage = safeStorage;
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const repoRoot = path.resolve(__dirname, '..');
const lifecycleGraphMethodsSource = fs.readFileSync(
path.join(repoRoot, 'web/resolver/shell/lifecycle_graph_methods.js'),
'utf8'
);
const queueMethodsSource = fs.readFileSync(
path.join(repoRoot, 'web/resolver/actions/queue_methods.js'),
'utf8'
);
function extractMethod(source, methodName, paramsPattern = '[^)]*') {
const signatureRegex = new RegExp(`\\n\\s+(?:async\\s+)?${methodName}\\s*\\(${paramsPattern}\\)\\s*\\{`);
const match = signatureRegex.exec(source);
assert.ok(match, `Could not find ${methodName}`);
const parenStart = source.indexOf('(', match.index);
const parenEnd = source.indexOf(')', parenStart);
const braceStart = source.indexOf('{', parenEnd);
let depth = 0;
for (let index = braceStart; index < source.length; index += 1) {
const char = source[index];
if (char === '{') depth += 1;
if (char === '}') depth -= 1;
if (depth === 0) {
const params = source.slice(parenStart + 1, parenEnd);
const isAsync = match[0].includes('async');
return `${isAsync ? 'async ' : ''}function ${methodName}(${params}) ${source.slice(braceStart, index + 1)}`;
}
}
throw new Error(`Could not parse ${methodName}`);
}
const animateTabContentTransition = eval(`(${extractMethod(queueMethodsSource, 'animateTabContentTransition')})`);
const closeDialog = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'close')})`);
const showContent = eval(`(${extractMethod(lifecycleGraphMethodsSource, 'showContent')})`);
test('switchTab calls animateTabContentTransition by default', () => {
let animationCalled = 0;
const ctx = {
...tabsLoadedMethods,
activeTab: 'missing',
getValidTab: (t) => t,
getTabButton: () => null,
persistActiveTab: () => {},
hideTooltip: () => {},
animateTabContentTransition: () => { animationCalled++; },
updateTabButtonStates: () => {},
updateQueueVisibility: () => {},
setMissingFooterControlsVisible: () => {},
loadLoadedModels: () => 'loaded-loaded',
contentElement: { style: {} },
};
const res = ctx.switchTab('loaded');
assert.equal(animationCalled, 1);
assert.equal(res, 'loaded-loaded');
assert.equal(ctx.activeTab, 'loaded');
});
test('switchTab skips animateTabContentTransition when animate is false', () => {
let animationCalled = 0;
const ctx = {
...tabsLoadedMethods,
activeTab: 'missing',
getValidTab: (t) => t,
getTabButton: () => null,
persistActiveTab: () => {},
hideTooltip: () => {},
animateTabContentTransition: () => { animationCalled++; },
updateTabButtonStates: () => {},
updateQueueVisibility: () => {},
setMissingFooterControlsVisible: () => {},
loadLoadedModels: () => 'loaded-loaded',
contentElement: { style: {} },
};
const res = ctx.switchTab('loaded', { force: true, animate: false });
assert.equal(animationCalled, 0);
assert.equal(res, 'loaded-loaded');
assert.equal(ctx.activeTab, 'loaded');
});
test('switchTab forwards workflow argument to loadWorkflowData and loadLoadedModels', () => {
let passedMissingWorkflow = null;
let passedLoadedWorkflow = null;
const customWorkflow = { nodes: [{ id: 42 }] };
const ctx = {
...tabsLoadedMethods,
activeTab: 'loaded',
getValidTab: (t) => t,
getTabButton: () => null,
persistActiveTab: () => {},
hideTooltip: () => {},
animateTabContentTransition: () => {},
updateTabButtonStates: () => {},
updateQueueVisibility: () => {},
setMissingFooterControlsVisible: () => {},
loadWorkflowData: (wf) => { passedMissingWorkflow = wf; },
loadLoadedModels: (wf) => { passedLoadedWorkflow = wf; },
contentElement: { style: {} },
};
ctx.switchTab('missing', { force: true, animate: false, workflow: customWorkflow });
assert.equal(passedMissingWorkflow, customWorkflow);
ctx.switchTab('loaded', { force: true, animate: false, workflow: customWorkflow });
assert.equal(passedLoadedWorkflow, customWorkflow);
});
test('animateTabContentTransition cancels any running animations on contentElement', () => {
let cancelCount = 0;
let animatedKeyframes = null;
let animatedOptions = null;
const dummyAnim = { cancel: () => { cancelCount++; } };
const ctx = {
contentElement: {
getAnimations: () => [dummyAnim, dummyAnim],
animate: (keyframes, opts) => {
animatedKeyframes = keyframes;
animatedOptions = opts;
},
},
};
animateTabContentTransition.call(ctx);
assert.equal(cancelCount, 2);
assert.deepEqual(animatedKeyframes, [
{ opacity: 0.72, transform: 'translateY(7px)' },
{ opacity: 1, transform: 'translateY(0)' }
]);
assert.equal(animatedOptions.duration, 180);
});
test('close cancels running animations on contentElement', () => {
let cancelCount = 0;
const dummyAnim = { cancel: () => { cancelCount++; } };
const ctx = {
docked: false,
dockContainer: null,
rememberSidebarOpenMode: () => {},
setDockDropPreviewActive: () => {},
setUndockDropPreviewActive: () => {},
hideTooltip: () => {},
backdrop: { style: {} },
element: { style: {} },
contentElement: {
getAnimations: () => [dummyAnim],
},
};
closeDialog.call(ctx);
assert.equal(cancelCount, 1);
assert.equal(ctx.element.style.display, 'none');
assert.equal(ctx.backdrop.style.display, 'none');
});
test('showContent restores loaded/metadata/options tabs without animation', async () => {
for (const targetTab of ['loaded', 'metadata', 'options']) {
const switchCalls = [];
const ctx = {
backdrop: { style: {} },
element: { style: {} },
scheduleModalViewportClamp: () => {},
loadFooterVersion: async () => {},
updateDownloadAllButtonState: () => {},
_serverSettingsLoaded: true,
ensureCapabilitiesLoaded: async () => {},
ensureBaseModelsLoaded: async () => {},
ensureAllModelsLoaded: async () => {},
ensureDownloadDirectoriesLoaded: async () => {},
attachDragHandleIfNeeded: () => {},
restoreActiveTab: () => targetTab,
updateTabButtonStates: () => {},
updateQueueVisibility: () => {},
syncWorkflowScopedQueue: () => {},
getCurrentWorkflow: () => ({ id: 'wf' }),
restoreActiveDownloadsFromBackend: async () => {},
switchTab: async (tab, opts) => {
switchCalls.push({ tab, opts });
},
};
await showContent.call(ctx);
assert.equal(switchCalls.length, 1);
assert.equal(switchCalls[0].tab, targetTab);
assert.equal(switchCalls[0].opts.animate, false);
assert.equal(switchCalls[0].opts.force, true);
}
});
+14 -16
View File
@@ -5083,7 +5083,7 @@ button.mr-info-link:disabled {
.mr-image-inspector-models .mr-copy-section {
padding-left: 0;
padding-right: 0;
background: transparent;
background: var(--mr-bg, #121212);
}
.mr-image-inspector-loading {
@@ -6081,20 +6081,18 @@ button.mr-info-link:disabled {
.mr-image-transfer-node-target {
display: flex;
align-items: center;
justify-content: space-between;
gap: var(--mr-spacing-8);
gap: var(--mr-spacing-10);
min-width: 0;
width: 100%;
cursor: pointer;
}
.mr-image-transfer-node-select {
appearance: none;
display: flex;
display: inline-flex;
align-items: center;
gap: var(--mr-spacing-10);
min-width: 0;
flex: 1 1 auto;
width: 100%;
justify-content: center;
flex-shrink: 0;
padding: 0;
border: 0;
background: transparent;
@@ -6105,16 +6103,15 @@ button.mr-info-link:disabled {
text-align: left;
}
#model-resolver-modal .mr-image-transfer-node-locate {
padding: var(--mr-spacing-2) var(--mr-spacing-6);
flex-shrink: 0;
opacity: 0.75;
transition: var(--mr-missing-control-transition, background 0.15s ease, border-color 0.15s ease, color 0.15s ease), opacity 0.15s ease;
.mr-image-transfer-node-header {
display: flex;
align-items: center;
min-width: 0;
max-width: 100%;
}
#model-resolver-modal .mr-image-transfer-node-locate:hover,
#model-resolver-modal .mr-image-transfer-node-locate:focus-visible {
opacity: 1;
#model-resolver-modal .mr-image-transfer-node-locate {
flex-shrink: 0;
}
.mr-image-transfer-node-select:disabled {
@@ -6147,6 +6144,7 @@ button.mr-info-link:disabled {
background: var(--mr-accent);
}
.mr-image-transfer-node-target:hover .mr-image-transfer-node-select:not(:disabled) .mr-image-transfer-node-check,
.mr-image-transfer-node-select:hover:not(:disabled) .mr-image-transfer-node-check {
border-color: rgba(114, 211, 138, 0.62);
}
+5
View File
@@ -2927,6 +2927,11 @@ export const queueMethods = {
if (!this.contentElement?.animate) return;
try {
if (typeof this.contentElement.getAnimations === 'function') {
for (const anim of this.contentElement.getAnimations()) {
anim.cancel();
}
}
this.contentElement.animate(
[
{ opacity: 0.72, transform: 'translateY(7px)' },
+64 -38
View File
@@ -14,53 +14,73 @@ export const lifecycleGraphMethods = {
},
async showContent(workflow = null, { restoreFullscreen = true, preserveCurrentView = false } = {}) {
this.backdrop.style.display = "none";
this.element.style.display = "flex";
void this.loadFooterVersion();
if (!this.docked) {
this.scheduleModalViewportClamp();
this._isOpeningContent = true;
if (this._workflowRefreshTimer) {
clearTimeout(this._workflowRefreshTimer);
this._workflowRefreshTimer = null;
}
if (this._workflowRefreshRetryTimer) {
clearTimeout(this._workflowRefreshRetryTimer);
this._workflowRefreshRetryTimer = null;
}
// Update button state in case there are active downloads
this.updateDownloadAllButtonState();
try {
this.backdrop.style.display = "none";
this.element.style.display = "flex";
void this.loadFooterVersion();
if (!this.docked) {
this.scheduleModalViewportClamp();
}
// On first open: load settings from server so tokens are available on
// every browser without re-entering them.
if (!this._serverSettingsLoaded) {
this._serverSettingsLoaded = true;
await this.loadSettingsFromServer();
}
// Update button state in case there are active downloads
this.updateDownloadAllButtonState();
// Ensure all models are loaded for dropdown
await this.ensureCapabilitiesLoaded();
await this.ensureBaseModelsLoaded();
await this.ensureAllModelsLoaded();
await this.ensureDownloadDirectoriesLoaded();
// On first open: load settings from server so tokens are available on
// every browser without re-entering them.
if (!this._serverSettingsLoaded) {
this._serverSettingsLoaded = true;
await this.loadSettingsFromServer();
}
const fs = safeStorage.getItem('model_resolver_modal_fullscreen');
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
// Ensure all models are loaded for dropdown
await this.ensureCapabilitiesLoaded();
await this.ensureBaseModelsLoaded();
await this.ensureAllModelsLoaded();
await this.ensureDownloadDirectoriesLoaded();
// Attach drag handle event listener (only once)
this.attachDragHandleIfNeeded();
const fs = safeStorage.getItem('model_resolver_modal_fullscreen');
if (!this.docked && restoreFullscreen && fs === '1') this.setFullScreen(true);
if (!preserveCurrentView) {
this.activeTab = this.restoreActiveTab();
}
this.updateTabButtonStates();
this.updateQueueVisibility();
this.syncWorkflowScopedQueue(workflow || this.getCurrentWorkflow());
await this.restoreActiveDownloadsFromBackend?.();
// Attach drag handle event listener (only once)
this.attachDragHandleIfNeeded();
if (preserveCurrentView) return;
if (!preserveCurrentView) {
this.activeTab = this.restoreActiveTab();
}
this.updateTabButtonStates();
this.updateQueueVisibility();
this.syncWorkflowScopedQueue(workflow || this.getCurrentWorkflow());
await this.restoreActiveDownloadsFromBackend?.();
if (this.activeTab === 'missing') {
await this.loadWorkflowData(workflow);
} else if (this.activeTab === 'loaded') {
this.switchTab('loaded', { force: true });
} else if (this.activeTab === 'metadata') {
this.switchTab('metadata', { force: true });
} else {
this.switchTab('options', { force: true });
if (preserveCurrentView) return;
if (this.activeTab === 'missing') {
const hasExistingBrowser = Boolean(
this.contentElement?.querySelector?.('.mr-missing-browser')
);
await this.loadWorkflowData(workflow, {
preserveContent: hasExistingBrowser
});
} else if (this.activeTab === 'loaded') {
await this.switchTab('loaded', { force: true, animate: false, workflow });
} else if (this.activeTab === 'metadata') {
await this.switchTab('metadata', { force: true, animate: false });
} else {
await this.switchTab('options', { force: true, animate: false });
}
} finally {
this._isOpeningContent = false;
this._lastOpenContentTimestamp = Date.now();
}
},
@@ -124,6 +144,12 @@ export const lifecycleGraphMethods = {
this._metadataPasteCleanup = null;
}
if (typeof this.contentElement?.getAnimations === 'function') {
for (const anim of this.contentElement.getAnimations()) {
anim.cancel();
}
}
if (wasDocked && collapseSidebar) {
this.closeComfySidebar(dockContainer);
}
+34 -7
View File
@@ -691,7 +691,11 @@ export const workflowStateMethods = {
nextRoute,
nextMissingSignature || nextSignature
);
const preserveSearchCache = Boolean(this.preserveSearchCacheAcrossNextWorkflowSync);
const sameRoute = Boolean(nextRoute && nextRoute === this.activeWorkflowRouteKey);
const preserveSearchCache = Boolean(
this.preserveSearchCacheAcrossNextWorkflowSync
|| sameRoute
);
const previousSearchCache = preserveSearchCache
? this.cloneSearchResultCache(this.searchResultCache, {
preserveActive: true,
@@ -730,6 +734,17 @@ export const workflowStateMethods = {
this.activeWorkflowSignature = nextSignature;
this.activeMissingWorkflowSignature = nextMissingSignature;
this.restorePendingQueueForActiveWorkflow();
if (sameRoute && previousKey && (!this.pendingResolutions || !this.pendingResolutions.length)) {
const previousPending = this.workflowPendingSelections?.get?.(previousKey);
if (Array.isArray(previousPending) && previousPending.length) {
this.pendingResolutions = this.clonePendingResolutions(previousPending);
this.savePendingQueueForActiveWorkflow?.();
this.rebuildPendingIndex?.();
this.updateApplyPendingButton?.();
this.updateQueuePanel?.();
this.updateQueueVisibility?.();
}
}
this.restoreAnalysisCacheForActiveWorkflow();
this.restoreLoadedModelsCacheForActiveWorkflow();
this.restoreSearchCacheForActiveWorkflow();
@@ -756,6 +771,13 @@ export const workflowStateMethods = {
}
this.preserveSearchCacheAcrossNextWorkflowSync = false;
this.restoreDownloadTargetSelectionsForActiveWorkflow();
if (sameRoute && previousKey && (!this.downloadTargetSelections || !this.downloadTargetSelections.size)) {
const previousTargets = this.workflowDownloadTargetSelections?.get?.(previousKey);
if (previousTargets && previousTargets.size) {
this.downloadTargetSelections = this.cloneDownloadTargetSelections(previousTargets);
this.saveDownloadTargetSelectionsForActiveWorkflow?.();
}
}
if (preservedActiveSearchMissingModels.length) {
this.syncSearchProgressAfterResume?.(preservedActiveSearchMissingModels, { workflowKey: nextKey });
}
@@ -816,6 +838,14 @@ export const workflowStateMethods = {
scheduleActiveWorkflowRefresh(reason = 'workflow-change') {
if (!this.isVisible() || Number(this._applyWorkflowRefreshSuppressionDepth || 0) > 0) return;
if (this._isOpeningContent) return;
if (
this._lastOpenContentTimestamp
&& (Date.now() - this._lastOpenContentTimestamp < 800)
&& ['window-focus', 'visibility-change'].includes(reason)
) {
return;
}
const generation = (this._workflowRefreshGeneration || 0) + 1;
this._workflowRefreshGeneration = generation;
@@ -924,13 +954,10 @@ export const workflowStateMethods = {
tab: this.activeTab
});
const preserveWorkflowContent = (
!routeChanged
&& String(reason || '').startsWith('node-')
);
const preserveWorkflowContent = !routeChanged;
this.preserveSearchCacheAcrossNextWorkflowSync = Boolean(
!routeChanged && reason === 'node-widget-change'
!routeChanged
);
this.syncWorkflowScopedQueue(workflow);
@@ -951,7 +978,7 @@ export const workflowStateMethods = {
} else if (this.activeTab === 'metadata') {
if (this.contentElement) this.contentElement.style.overflowY = 'auto';
await this.openMetadataTransfer?.({
preserveContent: !routeChanged && String(reason || '').startsWith('node-'),
preserveContent: !routeChanged,
});
}
}
+10 -1
View File
@@ -962,10 +962,19 @@ export const missingBrowserMethods = {
const currentDetail = currentBrowser.querySelector('.mr-missing-detail-pane');
const nextDetail = nextBrowser.querySelector('.mr-missing-detail-pane');
const activeElement = typeof document !== 'undefined' ? document.activeElement : null;
const isDetailActive = Boolean(
currentDetail && (
(activeElement && currentDetail.contains(activeElement) && (
['INPUT', 'TEXTAREA', 'SELECT'].includes(activeElement.tagName) || activeElement.isContentEditable
))
|| currentDetail.querySelector('.mr-tree-picker-dropdown:not([hidden]), [data-ml-dropdown-open], .mr-combo-list:not([hidden]), [aria-expanded="true"]')
)
);
this._missingBrowserDetailPreserved = Boolean(
currentDetail
&& nextDetail
&& currentDetail.outerHTML === nextDetail.outerHTML
&& (isDetailActive || currentDetail.outerHTML === nextDetail.outerHTML)
);
if (currentDetail && nextDetail && !this._missingBrowserDetailPreserved) {
currentDetail.innerHTML = nextDetail.innerHTML;
@@ -2429,36 +2429,46 @@ export const imageMetadataMethods = {
const locateTooltip = node.isTopLevel === false
? 'Open this subgraph and center the node in the ComfyUI graph.'
: 'Center this node in the ComfyUI graph.';
const locateButtonHtml = canLocate
const nodeDisplayTitle = nodeName === `Node ${node.nodeId}`
? `${nodeName}${scopeLabel}`
: `${nodeName} · Node ${node.nodeId}${scopeLabel}`;
const locateNodeHtml = canLocate
? `
<button type="button"
class="mr-node-chip is-locatable mr-missing-row-locate mr-image-transfer-node-locate"
class="mr-node-chip is-locatable mr-missing-row-node mr-missing-row-locate mr-image-transfer-node-locate"
data-image-transfer-locate
data-node-id="${this.escapeHtml(String(node.nodeId))}"
data-subgraph-id="${this.escapeHtml(String(node.subgraphId || ''))}"
data-is-top-level="${node.isTopLevel ? 'true' : 'false'}"
data-tooltip="${this.escapeHtml(locateTooltip)}"
aria-label="Center ${this.escapeHtml(nodeName)} in the ComfyUI graph">
${this.getLocateIconHtml()}
aria-label="Center ${this.escapeHtml(nodeDisplayTitle)} in the ComfyUI graph">
${this.getLocateIconHtml()}<span class="mr-missing-row-node-label">${this.escapeHtml(nodeDisplayTitle)}</span>
</button>
`
: '';
: `
<span class="mr-missing-row-node mr-image-transfer-node-name">${this.escapeHtml(nodeDisplayTitle)}</span>
`;
return `
<div class="mr-image-transfer-node-row${selected ? ' is-selected' : ''}" ${contextMenuAttrs}>
<div class="mr-image-transfer-node-target">
<div class="mr-image-transfer-node-target"
data-image-transfer-node="${this.escapeHtml(node.nodeKey)}"
data-image-transfer-target="${this.escapeHtml(targetKey)}"
data-image-transfer-category="${this.escapeHtml(normalizedCategory)}">
<button type="button" class="mr-image-transfer-node-select"
data-image-transfer-node="${this.escapeHtml(node.nodeKey)}"
data-image-transfer-target="${this.escapeHtml(targetKey)}"
data-image-transfer-category="${this.escapeHtml(normalizedCategory)}"
aria-pressed="${selected ? 'true' : 'false'}"
aria-label="Select ${this.escapeHtml(nodeDisplayTitle)} for model transfer"
aria-disabled="false">
<span class="mr-image-transfer-node-check" aria-hidden="true">${selected ? '✓' : ''}</span>
<span class="mr-image-transfer-node-details">
<span class="mr-image-transfer-node-name">${this.escapeHtml(nodeName)} · Node ${this.escapeHtml(node.nodeId)}${this.escapeHtml(scopeLabel)}</span>
<span class="mr-image-transfer-node-meta">${this.escapeHtml(nodeType)} · ${countText(node.refs.length, 'model slot')} · <span class="mr-image-transfer-node-status ${node.active ? 'is-active' : 'is-inactive'}">${node.active ? 'Active' : 'Inactive'}</span></span>
</span>
</button>
${locateButtonHtml}
<div class="mr-image-transfer-node-details">
<div class="mr-image-transfer-node-header">
${locateNodeHtml}
</div>
<span class="mr-image-transfer-node-meta">${this.escapeHtml(nodeType)} · ${countText(node.refs.length, 'model slot')} · <span class="mr-image-transfer-node-status ${node.active ? 'is-active' : 'is-inactive'}">${node.active ? 'Active' : 'Inactive'}</span></span>
</div>
</div>
<div class="mr-image-transfer-node-preview">${previewHtml}</div>
</div>
+6 -4
View File
@@ -38,7 +38,7 @@ export const tabsLoadedMethods = {
});
},
switchTab(tab, { force = false } = {}) {
switchTab(tab, { force = false, animate = true, workflow = null } = {}) {
const nextTab = this.getValidTab(tab);
const nextTabButton = this.getTabButton(nextTab);
if (
@@ -76,7 +76,9 @@ export const tabsLoadedMethods = {
}
}
this.hideTooltip();
this.animateTabContentTransition();
if (animate) {
this.animateTabContentTransition();
}
this.updateTabButtonStates();
this.updateQueueVisibility();
@@ -85,13 +87,13 @@ export const tabsLoadedMethods = {
this.contentElement.style.overflowY = 'auto';
}
this.setMissingFooterControlsVisible(true);
return this.loadWorkflowData();
return this.loadWorkflowData(workflow);
} else if (this.activeTab === 'loaded') {
if (this.contentElement) {
this.contentElement.style.overflowY = 'auto';
}
this.setMissingFooterControlsVisible(false);
return this.loadLoadedModels();
return this.loadLoadedModels(workflow);
} else if (this.activeTab === 'metadata') {
if (this.contentElement) {
this.contentElement.style.overflowY = 'auto';