From d9ca634077ed96843833188615e79415e696a2f1 Mon Sep 17 00:00:00 2001 From: Dariusz L Date: Wed, 5 Aug 2026 17:31:22 +0200 Subject: [PATCH] refactor frontend search progress rendering --- tests/test_search_retry.mjs | 52 +++++++++++++ web/resolver/search/search_panel.js | 117 +++++++++++++++------------- 2 files changed, 113 insertions(+), 56 deletions(-) diff --git a/tests/test_search_retry.mjs b/tests/test_search_retry.mjs index 8a7c109..5a442bf 100644 --- a/tests/test_search_retry.mjs +++ b/tests/test_search_retry.mjs @@ -29,11 +29,13 @@ function extractMethod(source, methodName, paramsPattern = '[^)]*') { } globalThis.getSvgIcon = () => ''; +const renderSearchProgressItem = eval(`(${extractMethod(searchPanelSource, 'renderSearchProgressItem', '[\\s\\S]*?')})`); const renderSearchProgress = eval(`(${extractMethod(searchPanelSource, 'renderSearchProgress')})`); const wireSearchProgressRetryButtons = eval(`(${extractMethod(searchPanelSource, 'wireSearchProgressRetryButtons', '[\\s\\S]*?')})`); function createDialog() { return { + renderSearchProgressItem, renderSearchProgress, wireSearchProgressRetryButtons, getSearchSourceLabel(source) { @@ -84,6 +86,56 @@ test('retryable provider failures render a source-specific retry action', () => assert.doesNotMatch(html, /data-source="civitai"[^>]*>.*Retry/); }); +test('active search progress renders the full progress rows', () => { + const dialog = createDialog(); + const html = dialog.renderSearchProgress({ + activeSearchRunId: 'run-42', + sourceProgress: { + civitai: { + status: 'running', + percent: 42, + message: 'Querying models', + }, + civarchive: { + status: 'error', + message: 'Search failed', + error: 'HTTP 522', + retryable: true, + }, + }, + }); + + assert.match(html, /class="mr-search-progress-list"/); + assert.doesNotMatch(html, /mr-search-progress-list-compact/); + assert.match(html, /aria-valuenow="42"/); + assert.match(html, /width: 42%/); + assert.match(html, /mr-search-progress-cancel/); + assert.match(html, /data-run-id="run-42"/); + assert.match(html, /mr-search-progress-retry/); +}); + +test('inactive search progress renders compact rows without progress controls', () => { + const dialog = createDialog(); + const html = dialog.renderSearchProgress({ + sourceProgress: { + civitai: { + status: 'found', + message: 'Found', + }, + civarchive: { + status: 'error', + message: 'Search failed', + retryable: true, + }, + }, + }); + + assert.match(html, /mr-search-progress-list-compact/); + assert.doesNotMatch(html, /mr-search-progress-bar/); + assert.doesNotMatch(html, /mr-search-progress-cancel/); + assert.match(html, /mr-search-progress-retry/); +}); + test('source retry action searches only the failed provider with force search', () => { const dialog = createDialog(); const listeners = {}; diff --git a/web/resolver/search/search_panel.js b/web/resolver/search/search_panel.js index 0f7a1d6..76a4174 100644 --- a/web/resolver/search/search_panel.js +++ b/web/resolver/search/search_panel.js @@ -1122,6 +1122,59 @@ export const searchPanelMethods = { )); }, + renderSearchProgressItem(source, progress, { + compact = false, + activeSearchRunId = null, + statusLabels = {}, + } = {}) { + const status = progress?.status || 'pending'; + const statusClass = String(status).replace(/[^a-z0-9_-]/gi, ''); + const label = this.getSearchSourceLabel(source); + const retryButton = status === 'error' && progress?.retryable + ? `` + : ''; + + if (compact) { + const statusLabel = progress?.message || statusLabels[status] || status; + const title = `${label}: ${progress?.error || statusLabel}`; + return ` +
+ ${this.escapeHtml(label)} + ${this.escapeHtml(statusLabel)} + ${retryButton} +
+ `; + } + + const canCancel = activeSearchRunId && (status === 'pending' || status === 'running'); + const percent = status === 'pending' + ? 0 + : (status === 'running' ? progress?.percent : 100); + const safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); + const runningMessage = progress?.message || 'Searching...'; + const statusLabel = status === 'running' + ? `${runningMessage} ${Math.round(safePercent)}%` + : (progress?.message || statusLabels[status] || status); + const title = progress?.error ? ` data-tooltip="${this.escapeHtml(`${label}: ${progress.error}`)}"` : ''; + const cancelButton = canCancel + ? `` + : ''; + + return ` +
+
+ ${this.escapeHtml(label)} + ${this.escapeHtml(statusLabel)} + ${cancelButton} + ${retryButton} +
+
+
+
+
+ `; + }, + renderSearchProgress(state = {}) { const progressEntries = Object.entries(state.sourceProgress || {}); if (!progressEntries.length) return ''; @@ -1136,63 +1189,15 @@ export const searchPanelMethods = { cancelled: 'Cancelled' }; - if (isCompact) { - let html = '
'; - for (const [source, progress] of progressEntries) { - const status = progress?.status || 'pending'; - const statusClass = String(status).replace(/[^a-z0-9_-]/gi, ''); - const label = this.getSearchSourceLabel(source); - const statusLabel = progress?.message || statusLabels[status] || status; - const title = `${label}: ${progress?.error || statusLabel}`; - const retryButton = status === 'error' && progress?.retryable - ? `` - : ''; - html += ` -
- ${this.escapeHtml(label)} - ${this.escapeHtml(statusLabel)} - ${retryButton} -
- `; - } - html += '
'; - return html; - } - - let html = '
'; + let html = isCompact + ? '
' + : '
'; for (const [source, progress] of progressEntries) { - const status = progress?.status || 'pending'; - const statusClass = String(status).replace(/[^a-z0-9_-]/gi, ''); - const label = this.getSearchSourceLabel(source); - const canCancel = state.activeSearchRunId && (status === 'pending' || status === 'running'); - const percent = status === 'pending' - ? 0 - : (status === 'running' ? progress?.percent : 100); - const safePercent = Math.max(0, Math.min(100, Number(percent) || 0)); - const runningMessage = progress?.message || 'Searching...'; - const statusLabel = status === 'running' - ? `${runningMessage} ${Math.round(safePercent)}%` - : (progress?.message || statusLabels[status] || status); - const title = progress?.error ? ` data-tooltip="${this.escapeHtml(`${label}: ${progress.error}`)}"` : ''; - const cancelButton = canCancel - ? `` - : ''; - const retryButton = status === 'error' && progress?.retryable - ? `` - : ''; - html += ` -
-
- ${this.escapeHtml(label)} - ${this.escapeHtml(statusLabel)} - ${cancelButton} - ${retryButton} -
-
-
-
-
- `; + html += this.renderSearchProgressItem(source, progress, { + compact: isCompact, + activeSearchRunId: state.activeSearchRunId, + statusLabels, + }); } html += '
'; return html;