diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 3762d00..cd178d7 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -2,7 +2,11 @@ import fs from 'node:fs'; import path from 'node:path'; import test from 'node:test'; import assert from 'node:assert/strict'; -import { html, normalizePathIdentity } from '../web/resolver/utils/html_utils.js'; +import { + html, + normalizePathIdentity, + safeStorage, +} from '../web/resolver/utils/html_utils.js'; import { getModelCardUrl } from '../web/resolver/utils/url_utils.js'; import { extractComfyWorkflow } from '../web/resolver/utils/workflow_metadata.js'; import { @@ -2320,6 +2324,133 @@ test('workflow model selection wait resolves only after the queued request compl assert.equal(completed.status, 'selected'); }); +test('Show in Model Resolver reuses the visible Missing Models browser without reloading', async () => { + const showResolvedNodeModelInResolver = eval( + `(${extractMethod(modelResolverSource, 'showResolvedNodeModelInResolver')})` + ); + const reference = { + node_id: 7, + widget_index: 0, + original_path: 'model.safetensors', + }; + const analysisData = { resolved_models: [reference] }; + const selectCalls = []; + let loadCount = 0; + let queueCount = 0; + const previousLocalStorageDescriptor = Object.getOwnPropertyDescriptor( + globalThis, + 'localStorage' + ); + Object.defineProperty(globalThis, 'localStorage', { + configurable: true, + value: { setItem() {} }, + }); + + const resolver = { + dialog: { + activeTab: 'missing', + cachedAnalysisData: analysisData, + isVisible: () => true, + persistActiveTab() {}, + selectWorkflowModelReference(...args) { + selectCalls.push(args); + return reference; + }, + queueWorkflowModelReferenceSelection() { + queueCount += 1; + return { status: 'pending' }; + }, + async loadWorkflowData() { + loadCount += 1; + }, + }, + getNodeContextWorkflowState: () => ({ signature: 'current' }), + getCurrentNodeContextAnalysis: () => analysisData, + waitForResolverDialogReady: () => { + throw new Error('The already visible browser should not wait for reopening'); + }, + }; + + try { + await showResolvedNodeModelInResolver.call(resolver, reference); + } finally { + if (previousLocalStorageDescriptor) { + Object.defineProperty( + globalThis, + 'localStorage', + previousLocalStorageDescriptor + ); + } else { + delete globalThis.localStorage; + } + } + + assert.equal(loadCount, 0); + assert.equal(queueCount, 0); + assert.equal(selectCalls.length, 1); + assert.equal(selectCalls[0][0], reference); + assert.equal(selectCalls[0][1], analysisData); + assert.deepEqual(selectCalls[0][2], { preferExistingBrowser: true }); +}); + +test('workflow model selection patches the existing browser and centers its row', () => { + const selectWorkflowModelReference = eval( + `(${extractMethod(missingBrowserMethodsSource, 'selectWorkflowModelReference')})` + ); + const selected = { + node_id: 7, + widget_index: 0, + original_path: 'model.safetensors', + }; + const row = { + dataset: { missingKey: 'selected-key' }, + scrollIntoViewOptions: null, + scrollIntoView(options) { + this.scrollIntoViewOptions = options; + }, + }; + const renderOptions = []; + const previousRequestAnimationFrame = globalThis.requestAnimationFrame; + globalThis.requestAnimationFrame = callback => callback(); + + const dialog = { + cachedAnalysisData: { resolved_models: [selected] }, + contentElement: { + querySelectorAll: () => [row], + }, + selectedMissingModelKey: 'previous-key', + getResolvedWorkflowModels: data => data.resolved_models, + getMissingModelKey: () => 'selected-key', + displayMissingModels(_container, _data, options) { + renderOptions.push(options); + }, + }; + + try { + assert.equal( + selectWorkflowModelReference.call( + dialog, + selected, + dialog.cachedAnalysisData, + { preferExistingBrowser: true } + ), + selected + ); + } finally { + if (previousRequestAnimationFrame) { + globalThis.requestAnimationFrame = previousRequestAnimationFrame; + } else { + delete globalThis.requestAnimationFrame; + } + } + + assert.deepEqual(renderOptions, [{ selectionOnly: true }]); + assert.deepEqual(row.scrollIntoViewOptions, { + block: 'center', + inline: 'nearest', + }); +}); + test('Local Database source ignores installed local model matches before search', () => { const hasMissingSourceSearchAttempt = eval(`(${extractMethod(missingBrowserMethodsSource, 'hasMissingSourceSearchAttempt')})`); const getMissingSourceResultStatus = eval(`(${extractMethod(missingBrowserMethodsSource, 'getMissingSourceResultStatus')})`); diff --git a/web/resolver/model_resolver.js b/web/resolver/model_resolver.js index 9c20b67..9ddcda6 100644 --- a/web/resolver/model_resolver.js +++ b/web/resolver/model_resolver.js @@ -654,17 +654,26 @@ export class ModelResolver { } this.dialog.persistActiveTab?.('missing'); + + if (wasVisible && wasOnMissingTab) { + const selected = this.dialog.selectWorkflowModelReference?.( + reference, + analysisData || this.dialog.cachedAnalysisData, + { preferExistingBrowser: true } + ); + if (selected) return; + } + this.dialog.showResolvedModels = true; this.dialog.missingModelsTypeFilter = 'all'; this.dialog.missingModelsTypeFilterMenuOpen = false; - const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference); - try { localStorage.setItem(this.dialog.showResolvedModelsStorageKey, '1'); } catch (error) { log.debug('Model Resolver: failed to persist resolved-model visibility.', error); } + const selectionRequest = this.dialog.queueWorkflowModelReferenceSelection?.(reference); if (!wasVisible) { this.dialog.activeTab = 'missing'; this.activateResolverButton(); diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 1f16b81..96eb4c0 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -538,23 +538,46 @@ export const missingBrowserMethods = { return selected; }, - selectWorkflowModelReference(reference = {}, data = this.cachedAnalysisData || {}) { + selectWorkflowModelReference( + reference = {}, + data = this.cachedAnalysisData || {}, + options = {} + ) { const selected = this.getResolvedWorkflowModels(data) .find(model => matchesWorkflowModelReference(model, reference)); if (!selected || !this.contentElement) return null; - this.showResolvedModels = true; - this.missingModelsTypeFilter = 'all'; - this.missingModelsTypeFilterMenuOpen = false; - safeStorage.setItem(this.showResolvedModelsStorageKey, '1'); - this.selectedMissingModelKey = this.getMissingModelKey(selected); - this.displayMissingModels(this.contentElement, data); + const selectedKey = this.getMissingModelKey(selected); + const selectionChanged = selectedKey !== this.selectedMissingModelKey; + const existingRow = Array.from( + this.contentElement.querySelectorAll?.('.mr-missing-list-row') || [] + ).find(item => item.dataset.missingKey === selectedKey); + const reuseExistingBrowser = Boolean( + options.preferExistingBrowser && existingRow + ); + this.selectedMissingModelKey = selectedKey; + + if (reuseExistingBrowser) { + if (selectionChanged) { + this.displayMissingModels( + this.contentElement, + data, + { selectionOnly: true } + ); + } + } else { + this.showResolvedModels = true; + this.missingModelsTypeFilter = 'all'; + this.missingModelsTypeFilterMenuOpen = false; + safeStorage.setItem(this.showResolvedModelsStorageKey, '1'); + this.displayMissingModels(this.contentElement, data); + } requestAnimationFrame(() => { const row = Array.from( this.contentElement?.querySelectorAll?.('.mr-missing-list-row') || [] ).find(item => item.dataset.missingKey === this.selectedMissingModelKey); - row?.scrollIntoView?.({ block: 'nearest' }); + row?.scrollIntoView?.({ block: 'center', inline: 'nearest' }); }); return selected; },