4aa3590017
Build and Push Docker Image / build (push) Successful in 53s
Docs Check / Markdown lint (push) Successful in 1m36s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m31s
CI / Security audit (push) Successful in 2m55s
Docs Check / Mermaid diagram parse check (push) Successful in 3m24s
CI / Swagger Validation & Coverage (push) Successful in 3m30s
CI / Tests & coverage (push) Successful in 3m50s
- Deleted redundant unit test file tests/unit/dashboard.test.js - Enabled skipped frontend DOM state and API tests in tests/frontend/state.test.js - Fixed Supertest client socket abort exception in SSE stream integration tests using new graceful testClose parameter - Consolidated duplicate helpers in server/routes/history.js and server/utils/arrRetrievers.js to unified services TagMatcher and DownloadAssembler - Added comprehensive unit tests for loadSecrets.js, PollingSonarrRetriever.js, PollingRadarrRetriever.js, and ombiHelpers.js - Achieved a 100% Vitest pass rate (834/834 tests) with robust code coverage
176 lines
5.3 KiB
JavaScript
176 lines
5.3 KiB
JavaScript
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
|
|
|
import { state } from '../state.js';
|
|
import { escapeHtml } from '../utils/format.js';
|
|
import { applyRequestFilters, getRequestStatus } from '../utils/ombiFilters.js';
|
|
|
|
/**
|
|
* Helper function to extract the username from an Ombi request object.
|
|
* The Ombi API returns requestedUser as an OmbiStore.Entities.OmbiUser object,
|
|
* not a string, so we need to extract the username from the object.
|
|
*
|
|
* Must stay in sync with server/utils/ombiHelpers.js
|
|
*
|
|
* @param {Object} request - The Ombi request object
|
|
* @returns {string} The extracted username, or empty string if not found
|
|
*/
|
|
function extractRequestedUser(request) {
|
|
if (!request) return '';
|
|
|
|
// Handle object format: OmbiStore.Entities.OmbiUser
|
|
if (request.requestedUser && typeof request.requestedUser === 'object') {
|
|
// Priority: alias > userAlias > userName > normalizedUserName > requestedByAlias
|
|
return request.requestedUser.alias ||
|
|
request.requestedUser.userAlias ||
|
|
request.requestedUser.userName ||
|
|
request.requestedUser.normalizedUserName ||
|
|
request.requestedByAlias || '';
|
|
}
|
|
// Handle string format (fallback for compatibility)
|
|
return request.requestedUser || request.requestedByAlias || '';
|
|
}
|
|
|
|
export function renderRequests() {
|
|
const requestsList = document.getElementById('requests-list');
|
|
const noRequests = document.getElementById('no-requests');
|
|
|
|
if (!requestsList) return;
|
|
|
|
const ombiRequests = state.ombiRequests || { movie: [], tv: [] };
|
|
const allRequests = [
|
|
...ombiRequests.movie.map(r => ({ ...r, mediaType: 'movie' })),
|
|
...ombiRequests.tv.map(r => ({ ...r, mediaType: 'tv' }))
|
|
];
|
|
|
|
// Apply client-side filters, sorting, and search
|
|
const filtered = applyRequestFilters(allRequests, {
|
|
types: state.selectedRequestTypes,
|
|
statuses: state.selectedRequestStatuses,
|
|
sort: state.requestSortMode,
|
|
search: state.requestSearchQuery
|
|
});
|
|
|
|
requestsList.innerHTML = '';
|
|
|
|
if (filtered.length === 0) {
|
|
if (noRequests) {
|
|
noRequests.style.display = 'block';
|
|
const p = noRequests.querySelector('p');
|
|
if (p) {
|
|
// Differentiate between no data from Ombi vs filters excluded everything
|
|
const hasAnyData = allRequests.length > 0;
|
|
p.textContent = hasAnyData
|
|
? 'No requests match your filters.'
|
|
: 'No requests found.';
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (noRequests) noRequests.style.display = 'none';
|
|
|
|
filtered.forEach(request => {
|
|
const card = createRequestCard(request);
|
|
requestsList.appendChild(card);
|
|
});
|
|
}
|
|
|
|
function createRequestCard(request) {
|
|
if (!request) {
|
|
const card = document.createElement('div');
|
|
card.className = 'request-card';
|
|
card.textContent = 'Invalid request data';
|
|
return card;
|
|
}
|
|
|
|
const card = document.createElement('div');
|
|
card.className = 'request-card';
|
|
|
|
const typeIcon = document.createElement('span');
|
|
typeIcon.className = `request-type-icon ${request.mediaType || ''}`;
|
|
typeIcon.textContent = request.mediaType === 'movie' ? '🎬' : '📺';
|
|
|
|
const content = document.createElement('div');
|
|
content.className = 'request-content';
|
|
|
|
const title = document.createElement('div');
|
|
title.className = 'request-title';
|
|
title.textContent = request.title || 'Unknown Title';
|
|
|
|
const meta = document.createElement('div');
|
|
meta.className = 'request-meta';
|
|
|
|
const statusBadge = createStatusBadge(request);
|
|
meta.appendChild(statusBadge);
|
|
|
|
if (request.year) {
|
|
const year = document.createElement('span');
|
|
year.className = 'request-year';
|
|
year.textContent = request.year;
|
|
meta.appendChild(year);
|
|
}
|
|
|
|
const username = extractRequestedUser(request);
|
|
if (username) {
|
|
const user = document.createElement('span');
|
|
user.className = 'request-user';
|
|
user.textContent = `Requested by: ${username}`;
|
|
meta.appendChild(user);
|
|
}
|
|
|
|
if (request.quality) {
|
|
const quality = document.createElement('span');
|
|
quality.className = 'request-quality';
|
|
quality.textContent = request.quality;
|
|
meta.appendChild(quality);
|
|
}
|
|
|
|
content.appendChild(title);
|
|
content.appendChild(meta);
|
|
|
|
const actions = document.createElement('div');
|
|
actions.className = 'request-actions';
|
|
|
|
if (state.ombiBaseUrl && request.theMovieDbId) {
|
|
const ombiLink = document.createElement('a');
|
|
ombiLink.className = 'request-link ombi-link';
|
|
ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType || 'movie'}/${request.theMovieDbId}`;
|
|
ombiLink.target = '_blank';
|
|
ombiLink.title = 'View in Ombi';
|
|
|
|
const ombiIcon = document.createElement('img');
|
|
ombiIcon.src = '/images/ombi.svg';
|
|
ombiIcon.alt = 'Ombi';
|
|
ombiIcon.className = 'request-icon';
|
|
|
|
ombiLink.appendChild(ombiIcon);
|
|
actions.appendChild(ombiLink);
|
|
}
|
|
|
|
card.appendChild(typeIcon);
|
|
card.appendChild(content);
|
|
card.appendChild(actions);
|
|
|
|
return card;
|
|
}
|
|
|
|
function createStatusBadge(request) {
|
|
const badge = document.createElement('span');
|
|
badge.className = 'request-status-badge';
|
|
|
|
const status = getRequestStatus(request);
|
|
const statusTexts = {
|
|
available: 'Available',
|
|
denied: `Denied: ${request.deniedReason || 'No reason'}`,
|
|
approved: 'Approved',
|
|
pending: 'Pending',
|
|
unknown: 'Unknown'
|
|
};
|
|
|
|
badge.classList.add(status);
|
|
badge.textContent = statusTexts[status] || 'Unknown';
|
|
|
|
return badge;
|
|
}
|
|
|