refactor frontend search progress rendering
This commit is contained in:
@@ -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 = {};
|
||||
|
||||
@@ -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
|
||||
? `<button type="button" class="mr-search-result-action-btn mr-search-progress-retry" data-source="${this.escapeHtml(source)}" data-tooltip="Retry ${this.escapeHtml(label)} search" aria-label="Retry ${this.escapeHtml(label)} search">${getSvgIcon('refreshCw')}</button>`
|
||||
: '';
|
||||
|
||||
if (compact) {
|
||||
const statusLabel = progress?.message || statusLabels[status] || status;
|
||||
const title = `${label}: ${progress?.error || statusLabel}`;
|
||||
return `
|
||||
<div class="mr-search-progress-item mr-search-progress-${statusClass}" data-search-progress-source="${this.escapeHtml(source)}" data-tooltip="${this.escapeHtml(title)}">
|
||||
<span class="mr-search-progress-source">${this.escapeHtml(label)}</span>
|
||||
<span class="mr-search-progress-status">${this.escapeHtml(statusLabel)}</span>
|
||||
${retryButton}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
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
|
||||
? `<button type="button" class="mr-search-result-action-btn mr-search-progress-cancel" data-source="${this.escapeHtml(source)}" data-run-id="${this.escapeHtml(activeSearchRunId)}" data-tooltip="Cancel ${this.escapeHtml(label)} search" aria-label="Cancel ${this.escapeHtml(label)} search">${getSvgIcon('x')}</button>`
|
||||
: '';
|
||||
|
||||
return `
|
||||
<div class="mr-search-progress-item mr-search-progress-${statusClass}" data-search-progress-source="${this.escapeHtml(source)}"${title}>
|
||||
<div class="mr-search-progress-head">
|
||||
<span class="mr-search-progress-source">${this.escapeHtml(label)}</span>
|
||||
<span class="mr-search-progress-status">${this.escapeHtml(statusLabel)}</span>
|
||||
${cancelButton}
|
||||
${retryButton}
|
||||
</div>
|
||||
<div class="mr-search-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${safePercent}">
|
||||
<div class="mr-search-progress-fill" style="width: ${safePercent}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
},
|
||||
|
||||
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 = '<div class="mr-search-progress-list mr-search-progress-list-compact">';
|
||||
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
|
||||
? `<button type="button" class="mr-search-result-action-btn mr-search-progress-retry" data-source="${this.escapeHtml(source)}" data-tooltip="Retry ${this.escapeHtml(label)} search" aria-label="Retry ${this.escapeHtml(label)} search">${getSvgIcon('refreshCw')}</button>`
|
||||
: '';
|
||||
html += `
|
||||
<div class="mr-search-progress-item mr-search-progress-${statusClass}" data-search-progress-source="${this.escapeHtml(source)}" data-tooltip="${this.escapeHtml(title)}">
|
||||
<span class="mr-search-progress-source">${this.escapeHtml(label)}</span>
|
||||
<span class="mr-search-progress-status">${this.escapeHtml(statusLabel)}</span>
|
||||
${retryButton}
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
}
|
||||
|
||||
let html = '<div class="mr-search-progress-list">';
|
||||
let html = isCompact
|
||||
? '<div class="mr-search-progress-list mr-search-progress-list-compact">'
|
||||
: '<div class="mr-search-progress-list">';
|
||||
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
|
||||
? `<button type="button" class="mr-search-result-action-btn mr-search-progress-cancel" data-source="${this.escapeHtml(source)}" data-run-id="${this.escapeHtml(state.activeSearchRunId)}" data-tooltip="Cancel ${this.escapeHtml(label)} search" aria-label="Cancel ${this.escapeHtml(label)} search">${getSvgIcon('x')}</button>`
|
||||
: '';
|
||||
const retryButton = status === 'error' && progress?.retryable
|
||||
? `<button type="button" class="mr-search-result-action-btn mr-search-progress-retry" data-source="${this.escapeHtml(source)}" data-tooltip="Retry ${this.escapeHtml(label)} search" aria-label="Retry ${this.escapeHtml(label)} search">${getSvgIcon('refreshCw')}</button>`
|
||||
: '';
|
||||
html += `
|
||||
<div class="mr-search-progress-item mr-search-progress-${statusClass}" data-search-progress-source="${this.escapeHtml(source)}"${title}>
|
||||
<div class="mr-search-progress-head">
|
||||
<span class="mr-search-progress-source">${this.escapeHtml(label)}</span>
|
||||
<span class="mr-search-progress-status">${this.escapeHtml(statusLabel)}</span>
|
||||
${cancelButton}
|
||||
${retryButton}
|
||||
</div>
|
||||
<div class="mr-search-progress-bar" role="progressbar" aria-valuemin="0" aria-valuemax="100" aria-valuenow="${safePercent}">
|
||||
<div class="mr-search-progress-fill" style="width: ${safePercent}%;"></div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
html += this.renderSearchProgressItem(source, progress, {
|
||||
compact: isCompact,
|
||||
activeSearchRunId: state.activeSearchRunId,
|
||||
statusLabels,
|
||||
});
|
||||
}
|
||||
html += '</div>';
|
||||
return html;
|
||||
|
||||
Reference in New Issue
Block a user