From b93f63e68a8ee4ca77915ad1f26bd1535ea5fe92 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Thu, 10 Sep 2026 01:16:31 +0200 Subject: [PATCH] fix: keep Apply button disabled during queued resolution and re-enable on failure --- tests/test_downloads_tab_workflow_route.mjs | 49 +++++++++++++++++++++ web/resolver/actions/queue_methods.js | 5 ++- 2 files changed, 53 insertions(+), 1 deletion(-) diff --git a/tests/test_downloads_tab_workflow_route.mjs b/tests/test_downloads_tab_workflow_route.mjs index a69ba48..7c63881 100644 --- a/tests/test_downloads_tab_workflow_route.mjs +++ b/tests/test_downloads_tab_workflow_route.mjs @@ -1952,6 +1952,55 @@ test('Apply reuses the content-preserving workflow analysis path', () => { assert.doesNotMatch(applyPendingResolutionList, /refreshAnalysisInBackground\(/); }); +test('Apply stays busy after linking until the old card is removed', async () => { + const applyQueuedByKey = eval(`(${extractMethod(queueMethodsSource, 'applyQueuedByKey')})`); + const window = new Window(); + const button = window.document.createElement('button'); + button.textContent = 'Apply'; + window.document.body.append(button); + let finishApply; + let calls = 0; + const dialog = { + pendingIndex: new Map([['model', 0]]), + pendingResolutions: [{}], + async applyPendingResolutionList() { + calls += 1; + await new Promise(resolve => { finishApply = resolve; }); + this.pendingIndex.delete('model'); + } + }; + const pending = applyQueuedByKey.call(dialog, 'model', button); + assert.equal(button.textContent, 'Applying...'); + assert.equal(button.disabled, true); + await applyQueuedByKey.call(dialog, 'model', button); + assert.equal(calls, 1); + finishApply(); + await pending; + assert.equal(button.isConnected, true); + assert.equal(button.textContent, 'Applying...'); + assert.equal(button.disabled, true); + assert.equal(button.classList.contains('mr-btn-is-disabled'), true); + button.remove(); + await window.happyDOM.close(); +}); + +test('Apply becomes available again when linking fails and selection stays queued', async () => { + const applyQueuedByKey = eval(`(${extractMethod(queueMethodsSource, 'applyQueuedByKey')})`); + const window = new Window(); + const button = window.document.createElement('button'); + window.document.body.append(button); + const dialog = { + pendingIndex: new Map([['model', 0]]), + pendingResolutions: [{}], + async applyPendingResolutionList() { return null; } + }; + await applyQueuedByKey.call(dialog, 'model', button); + assert.equal(button.textContent, 'Apply'); + assert.equal(button.disabled, false); + assert.equal(button.classList.contains('mr-btn-is-disabled'), false); + await window.happyDOM.close(); +}); + test('Apply keeps the workflow slot key when resolved path identity changes', () => { const doesResolvedModelMatchAlias = eval( `(${extractMethod(queueMethodsSource, 'doesResolvedModelMatchAlias')})` diff --git a/web/resolver/actions/queue_methods.js b/web/resolver/actions/queue_methods.js index adfee1e..a310218 100644 --- a/web/resolver/actions/queue_methods.js +++ b/web/resolver/actions/queue_methods.js @@ -2096,6 +2096,7 @@ export const queueMethods = { * Apply one queued resolution from the selected bar. */ async applyQueuedByKey(key, button = null) { + if (button?.disabled) return; if (!key || !this.pendingIndex.has(key)) { this.showNotification('Selected match is no longer queued.', 'error'); return; @@ -2117,7 +2118,9 @@ export const queueMethods = { try { await this.applyPendingResolutionList([selection], { clearAll: false }); } finally { - if (button?.isConnected) { + // A successful link removes the selection before the background + // analysis replaces this card. Keep it busy until that render. + if (button?.isConnected && this.pendingIndex.has(key)) { button.disabled = false; button.classList.remove('mr-btn-is-disabled'); button.textContent = 'Apply';