feat: Add Ombi request filtering and search
Build and Push Docker Image / build (push) Successful in 1m29s
Docs Check / Markdown lint (push) Successful in 1m51s
Licence Check / Licence compatibility and copyright header verification (push) Failing after 2m3s
CI / Security audit (push) Successful in 2m54s
CI / Swagger Validation & Coverage (push) Successful in 3m6s
Docs Check / Mermaid diagram parse check (push) Successful in 3m13s
CI / Tests & coverage (push) Successful in 3m31s

- Add request filters UI (type, status, sort, search)
- Implement dual-layer filtering (server + client)
- Add ombiFilters utility for consistent filtering logic
- Persist filter preferences in localStorage
- Add SSE support for real-time Ombi request updates
- Add webhook endpoints for Ombi integration
- Update OpenAPI spec for new endpoints
- Add unit tests for filter logic and UI
- Add integration tests for Ombi routes
This commit is contained in:
2026-05-22 12:31:31 +01:00
parent dbf45ec31d
commit d3d085d614
17 changed files with 1695 additions and 83 deletions
+35 -42
View File
@@ -2,12 +2,15 @@
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
*/
@@ -38,16 +41,34 @@ export function renderRequests() {
...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 (allRequests.length === 0) {
if (noRequests) noRequests.style.display = 'block';
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';
allRequests.forEach(request => {
filtered.forEach(request => {
const card = createRequestCard(request);
requestsList.appendChild(card);
});
@@ -102,7 +123,7 @@ function createRequestCard(request) {
const actions = document.createElement('div');
actions.className = 'request-actions';
if (request.theMovieDbId) {
if (state.ombiBaseUrl && request.theMovieDbId) {
const ombiLink = document.createElement('a');
ombiLink.className = 'request-link ombi-link';
ombiLink.href = `${state.ombiBaseUrl}/details/${request.mediaType}/${request.theMovieDbId}`;
@@ -129,46 +150,18 @@ function createStatusBadge(request) {
const badge = document.createElement('span');
badge.className = 'request-status-badge';
let status = 'unknown';
let text = 'Unknown';
if (request.available) {
status = 'available';
text = 'Available';
} else if (request.approved) {
status = 'approved';
text = 'Approved';
} else if (request.denied) {
status = 'denied';
text = `Denied: ${request.deniedReason || 'No reason'}`;
} else if (request.requested) {
status = 'pending';
text = 'Pending';
}
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 = text;
badge.textContent = statusTexts[status] || 'Unknown';
return badge;
}
export function setupRequestsTab() {
// Listen for SSE updates
if (state.sseSource) {
state.sseSource.addEventListener('message', (event) => {
const data = JSON.parse(event.data);
if (data.ombiRequests) {
state.ombiRequests = data.ombiRequests;
renderRequests();
}
});
}
// Also listen for custom event triggered from sse.js
document.addEventListener('ombiRequestsUpdated', () => {
renderRequests();
});
// Initial render
renderRequests();
}