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
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:
@@ -0,0 +1,107 @@
|
||||
// Copyright (c) 2026 Gordon Bolton. MIT License.
|
||||
/**
|
||||
* Pure filter / sort / search utilities for Ombi requests.
|
||||
* Must stay in sync with server/utils/ombiFilters.js
|
||||
*/
|
||||
|
||||
/**
|
||||
* Derive a single status string from an Ombi request object.
|
||||
* Priority: available > denied > approved > pending > unknown
|
||||
*
|
||||
* @param {Object} request
|
||||
* @returns {string}
|
||||
*/
|
||||
export function getRequestStatus(request) {
|
||||
if (!request) return 'unknown';
|
||||
if (request.available) return 'available';
|
||||
if (request.denied) return 'denied';
|
||||
if (request.approved) return 'approved';
|
||||
if (request.requested) return 'pending';
|
||||
return 'unknown';
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter requests by media type.
|
||||
*
|
||||
* @param {Array} requests
|
||||
* @param {string[]} types
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function filterByType(requests, types) {
|
||||
if (!types || types.length === 0) return requests;
|
||||
const normalized = types.map(t => t.toLowerCase());
|
||||
if (normalized.includes('all')) return requests;
|
||||
return requests.filter(r => normalized.includes(r.mediaType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter requests by status.
|
||||
*
|
||||
* @param {Array} requests
|
||||
* @param {string[]} statuses
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function filterByStatus(requests, statuses) {
|
||||
if (!statuses || statuses.length === 0) return requests;
|
||||
const normalized = statuses.map(s => s.toLowerCase());
|
||||
return requests.filter(r => normalized.includes(getRequestStatus(r)));
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter requests by case-insensitive title substring.
|
||||
*
|
||||
* @param {Array} requests
|
||||
* @param {string} query
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function filterBySearch(requests, query) {
|
||||
if (!query || query.trim() === '') return requests;
|
||||
const q = query.trim().toLowerCase();
|
||||
return requests.filter(r => (r.title || '').toLowerCase().includes(q));
|
||||
}
|
||||
|
||||
/**
|
||||
* Sort requests by the given sort mode.
|
||||
*
|
||||
* @param {Array} requests
|
||||
* @param {string} sortMode
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function sortRequests(requests, sortMode) {
|
||||
const sorted = [...requests];
|
||||
switch (sortMode) {
|
||||
case 'requestedDate_asc':
|
||||
return sorted.sort((a, b) => {
|
||||
const da = a.requestedDate ? new Date(a.requestedDate).getTime() : 0;
|
||||
const db = b.requestedDate ? new Date(b.requestedDate).getTime() : 0;
|
||||
return da - db;
|
||||
});
|
||||
case 'title_asc':
|
||||
return sorted.sort((a, b) => (a.title || '').localeCompare(b.title || ''));
|
||||
case 'title_desc':
|
||||
return sorted.sort((a, b) => (b.title || '').localeCompare(a.title || ''));
|
||||
case 'requestedDate_desc':
|
||||
default:
|
||||
return sorted.sort((a, b) => {
|
||||
const da = a.requestedDate ? new Date(a.requestedDate).getTime() : 0;
|
||||
const db = b.requestedDate ? new Date(b.requestedDate).getTime() : 0;
|
||||
return db - da;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply all filters and sorting in one call.
|
||||
*
|
||||
* @param {Array} requests
|
||||
* @param {Object} options
|
||||
* @returns {Array}
|
||||
*/
|
||||
export function applyRequestFilters(requests, { types, statuses, sort, search } = {}) {
|
||||
let result = [...requests];
|
||||
result = filterByType(result, types);
|
||||
result = filterByStatus(result, statuses);
|
||||
result = filterBySearch(result, search);
|
||||
result = sortRequests(result, sort);
|
||||
return result;
|
||||
}
|
||||
@@ -46,6 +46,41 @@ import { state } from '../state.js';
|
||||
}
|
||||
})();
|
||||
|
||||
// Load request filter preferences from localStorage
|
||||
(function loadRequestFilters() {
|
||||
try {
|
||||
const savedTypes = localStorage.getItem('sofarr-request-types');
|
||||
if (savedTypes) state.selectedRequestTypes = JSON.parse(savedTypes);
|
||||
} catch (e) {
|
||||
console.error('[Storage] Failed to load request types:', e);
|
||||
state.selectedRequestTypes = ['movie', 'tv'];
|
||||
}
|
||||
|
||||
try {
|
||||
const savedStatuses = localStorage.getItem('sofarr-request-statuses');
|
||||
if (savedStatuses) state.selectedRequestStatuses = JSON.parse(savedStatuses);
|
||||
} catch (e) {
|
||||
console.error('[Storage] Failed to load request statuses:', e);
|
||||
state.selectedRequestStatuses = [];
|
||||
}
|
||||
|
||||
try {
|
||||
const savedSort = localStorage.getItem('sofarr-request-sort');
|
||||
if (savedSort) state.requestSortMode = savedSort;
|
||||
} catch (e) {
|
||||
console.error('[Storage] Failed to load request sort:', e);
|
||||
state.requestSortMode = 'requestedDate_desc';
|
||||
}
|
||||
|
||||
try {
|
||||
const savedSearch = localStorage.getItem('sofarr-request-search');
|
||||
if (savedSearch !== null) state.requestSearchQuery = savedSearch;
|
||||
} catch (e) {
|
||||
console.error('[Storage] Failed to load request search:', e);
|
||||
state.requestSearchQuery = '';
|
||||
}
|
||||
})();
|
||||
|
||||
// Export helper functions for localStorage operations
|
||||
export function saveHistoryDays(days) {
|
||||
localStorage.setItem('sofarr-history-days', days);
|
||||
@@ -74,3 +109,19 @@ export function getActiveTab() {
|
||||
export function saveActiveTab(tab) {
|
||||
localStorage.setItem('sofarr-active-tab', tab);
|
||||
}
|
||||
|
||||
export function saveRequestTypes(types) {
|
||||
localStorage.setItem('sofarr-request-types', JSON.stringify(types));
|
||||
}
|
||||
|
||||
export function saveRequestStatuses(statuses) {
|
||||
localStorage.setItem('sofarr-request-statuses', JSON.stringify(statuses));
|
||||
}
|
||||
|
||||
export function saveRequestSort(sort) {
|
||||
localStorage.setItem('sofarr-request-sort', sort);
|
||||
}
|
||||
|
||||
export function saveRequestSearch(query) {
|
||||
localStorage.setItem('sofarr-request-search', query);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user