From 9f9b3da12c191110f6948c73d094b9a574cc5f17 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 10 Sep 2026 14:30:33 +0200 Subject: [PATCH] Use getMissingModelDomKey for search UI element IDs Replace inline node_id/widget_index DOM key construction with the centralized getMissingModelDomKey helper across search panel and missing browser methods. This keeps search results and button IDs stable when the same model appears in multiple workflows/widgets. Also adds a regression test verifying that search progress stays with its model when switching between shared widgets. --- tests/test_downloads_tab_workflow_route.mjs | 37 +++++++++++++++++++ .../actions/resolve_download_methods.js | 8 ++-- web/resolver/search/search_panel.js | 11 +++--- web/resolver/views/missing_browser_methods.js | 6 +-- 4 files changed, 50 insertions(+), 12 deletions(-) diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index 7c63881..2c51212 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -7785,6 +7785,7 @@ test('info dialog size formatting preserves numeric, nested and label values', ( test('link/name controls place the add or search action before the input switch', () => { const renderSearchControls = eval(`(${extractMethod(searchPanelMethodsSource, 'renderSearchControls')})`); const dialog = { + getMissingModelDomKey() { return 'model-key'; }, getSearchState() { return { selectedSource: 'all', @@ -7838,6 +7839,42 @@ test('link/name controls place the add or search action before the input switch' assert.ok(modeIndex > inputIndex); }); +test('search progress stays with its model when switching a shared widget', async () => { + const refresh = eval(`(${extractMethod(searchPanelMethodsSource.replaceAll('workflowKey = this.getWorkflowScopedQueueKey()', "workflowKey = 'workflow'"), 'refreshSearchUiForMissingNow')})`); + const window = new Window(); + const root = window.document.createElement('div'); + window.document.body.append(root); + const first = { node_id: 232, widget_index: 0, original_path: 'first' }; + const second = { ...first, original_path: 'second' }; + const state = { activeSearchRunId: 'run-a' }; + const dialog = { + ...workflowIdentityMethods, + contentElement: root, + activeTab: 'missing', + getWorkflowScopedQueueKey: () => 'workflow', + getMissingSearchKey: model => model.original_path, + refreshMissingSourcesSummary() {}, + hasRenderableSearchState: () => true, + displaySearchResults(model, currentState, container) { container.textContent = model.original_path; }, + renderSearchButtonContent: text => text, + }; + const mount = model => { + dialog.selectedMissingModelKey = dialog.getMissingModelKey(model); + const key = dialog.getMissingModelDomKey(model); + root.innerHTML = `
`; + }; + mount(second); + refresh.call(dialog, first, state, { workflowKey: 'workflow' }); + assert.equal(root.querySelector('div').textContent, ''); + assert.equal(root.querySelector('button').disabled, false); + mount(first); + refresh.call(dialog, first, state, { workflowKey: 'workflow' }); + assert.equal(root.querySelector('div').textContent, 'first'); + assert.equal(root.querySelector('button').disabled, true); + assert.notEqual(dialog.getMissingModelDomKey(first), dialog.getMissingModelDomKey(second)); + await window.happyDOM.close(); +}); + test('search result refreshes preserve keyed progress and result DOM', () => { const displaySearchResults = extractMethod(resolveDownloadMethodsSource, 'displaySearchResults'); const patchSearchResultsContainer = extractMethod( diff --git a/web/resolver/actions/resolve_download_methods.js b/web/resolver/actions/resolve_download_methods.js index 89b73a9..0837fc5 100644 --- a/web/resolver/actions/resolve_download_methods.js +++ b/web/resolver/actions/resolve_download_methods.js @@ -2645,13 +2645,13 @@ export const resolveDownloadMethods = { } const isUrn = Boolean(missing.is_urn && !requestedSearchQuery && !searchSha256); - const resultsId = `search-results-${missing.node_id}-${missing.widget_index}`; + const resultsId = `search-results-${this.getMissingModelDomKey(missing)}`; const canUpdateCurrentWorkflow = workflowKey === this.getWorkflowScopedQueueKey(); const resultsDiv = canUpdateCurrentWorkflow ? this.contentElement?.querySelector(`#${resultsId}`) : null; const searchBtn = canUpdateCurrentWorkflow - ? this.contentElement?.querySelector(`#search-${missing.node_id}-${missing.widget_index}`) + ? this.contentElement?.querySelector(`#search-${this.getMissingModelDomKey(missing)}`) : null; let searchRunId = null; let completedSearchRun = false; @@ -3192,13 +3192,13 @@ export const resolveDownloadMethods = { wireDownloadSearchPanel(container, missing) { if (!container) return; - const searchResultsId = `search-results-${missing.node_id}-${missing.widget_index}`; + const searchResultsId = `search-results-${this.getMissingModelDomKey(missing)}`; const searchResultsDiv = container.querySelector(`#${searchResultsId}`); if (searchResultsDiv) { this.wireSearchDownloadButtons(searchResultsDiv, missing); } - const searchBtn = container.querySelector(`#search-${missing.node_id}-${missing.widget_index}`); + const searchBtn = container.querySelector(`#search-${this.getMissingModelDomKey(missing)}`); if (searchBtn && searchBtn.dataset.mlSearchBound !== 'true') { searchBtn.dataset.mlSearchBound = 'true'; searchBtn.addEventListener('click', () => { diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 9943921..33fca23 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -596,8 +596,9 @@ export const searchPanelMethods = { this.updateBatchFooterButtons?.(); if (!this.contentElement || this.activeTab !== 'missing') return; + if (this.selectedMissingModelKey && this.getMissingModelKey(missing) !== this.selectedMissingModelKey) return; - const resultsId = `search-results-${missing.node_id}-${missing.widget_index}`; + const resultsId = `search-results-${this.getMissingModelDomKey(missing)}`; const resultsDiv = this.contentElement.querySelector(`#${resultsId}`); if (resultsDiv && currentState && this.hasRenderableSearchState(currentState)) { resultsDiv.classList.remove('mr-is-hidden'); @@ -605,7 +606,7 @@ export const searchPanelMethods = { this.displaySearchResults(missing, currentState, resultsDiv); } - const searchBtn = this.contentElement.querySelector(`#search-${missing.node_id}-${missing.widget_index}`); + const searchBtn = this.contentElement.querySelector(`#search-${this.getMissingModelDomKey(missing)}`); if (searchBtn && currentState) { const isRunning = Boolean(currentState.activeSearchRunId); const hasSearchAttempt = this.hasRenderableSearchState(currentState); @@ -1915,7 +1916,7 @@ export const searchPanelMethods = { html += ``; const downloadSourceRow = this.getDownloadSourceTableRow(missing, downloadSource); - html += `
`; + html += `
`; html += this.renderSearchResultsTable(downloadSourceRow ? [downloadSourceRow] : []); html += `
`; return html; @@ -2082,8 +2083,8 @@ export const searchPanelMethods = { let html = `
`; html += `
`; - html += ``; - html += ``; html += `
`; diff --git a/web/resolver/views/missing_browser_methods.js b/web/resolver/views/missing_browser_methods.js index 9f83966..c402800 100644 --- a/web/resolver/views/missing_browser_methods.js +++ b/web/resolver/views/missing_browser_methods.js @@ -2260,7 +2260,7 @@ export const missingBrowserMethods = { } const state = this.searchResultCache.get(this.getMissingSearchKey(missing)); - const searchResultsDiv = container.querySelector(`#search-results-${missing.node_id}-${missing.widget_index}`); + const searchResultsDiv = container.querySelector(`#search-results-${this.getMissingModelDomKey(missing)}`); if (state && searchResultsDiv && this.hasRenderableSearchState(state)) { searchResultsDiv.classList.remove('mr-is-hidden'); searchResultsDiv.classList.add('mr-is-visible'); @@ -2732,7 +2732,7 @@ export const missingBrowserMethods = { }); html += this.renderDownloadTargetControls(missing, missing.category || 'checkpoints'); html += `
`; - html += `
`; + html += `
`; } else if (missing.is_urn) { html += `
`; html += `
Resolving CivitAI download for this URN...
`; @@ -2743,7 +2743,7 @@ export const missingBrowserMethods = { html += this.renderSearchControls(missing); html += this.renderDownloadTargetControls(missing, missing.category || 'checkpoints'); html += `
`; - html += `
`; + html += `
`; } // Progress container (for downloads). Use the complete model identity,