mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 02:41:11 +01:00
feat(scoring): implement sorting and filtering for format groups
This commit is contained in:
@@ -1,12 +1,14 @@
|
||||
import React, { useState, useCallback, memo } from 'react';
|
||||
import React, { useState, useCallback, memo, useMemo } from 'react';
|
||||
import PropTypes from 'prop-types';
|
||||
import { ChevronDown, Volume2, Monitor, Users, Tv, Code, HardDrive, Tag, Square, Layers, Database, Folder } from 'lucide-react';
|
||||
import { ChevronDown, ChevronUp, ChevronsUpDown, Volume2, Monitor, Users, Tv, Code, HardDrive, Tag, Square, Layers, Database, Folder } from 'lucide-react';
|
||||
import NumberInput from '@ui/NumberInput';
|
||||
import Tooltip from '@ui/Tooltip';
|
||||
import { Copy } from 'lucide-react';
|
||||
|
||||
const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, icon }) => {
|
||||
const [isExpanded, setIsExpanded] = useState(true);
|
||||
const [sortColumn, setSortColumn] = useState('radarr');
|
||||
const [sortDirection, setSortDirection] = useState('desc');
|
||||
|
||||
// Map group names to icons
|
||||
const groupIcons = {
|
||||
@@ -45,6 +47,60 @@ const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, i
|
||||
}
|
||||
}, [formats, onScoreChange]);
|
||||
|
||||
const handleSort = useCallback((column) => {
|
||||
if (sortColumn === column) {
|
||||
setSortDirection(prev => prev === 'asc' ? 'desc' : 'asc');
|
||||
} else {
|
||||
setSortColumn(column);
|
||||
setSortDirection('asc');
|
||||
}
|
||||
}, [sortColumn]);
|
||||
|
||||
// Sort formats based on current sort settings
|
||||
const sortedFormats = useMemo(() => {
|
||||
const sorted = [...formats].sort((a, b) => {
|
||||
let aValue, bValue;
|
||||
|
||||
switch (sortColumn) {
|
||||
case 'name':
|
||||
aValue = a.name.toLowerCase();
|
||||
bValue = b.name.toLowerCase();
|
||||
break;
|
||||
case 'radarr':
|
||||
// Sort by enabled status first, then by score
|
||||
if (a.radarr && !b.radarr) return -1;
|
||||
if (!a.radarr && b.radarr) return 1;
|
||||
aValue = a.radarrScore ?? a.score ?? 0;
|
||||
bValue = b.radarrScore ?? b.score ?? 0;
|
||||
break;
|
||||
case 'sonarr':
|
||||
// Sort by enabled status first, then by score
|
||||
if (a.sonarr && !b.sonarr) return -1;
|
||||
if (!a.sonarr && b.sonarr) return 1;
|
||||
aValue = a.sonarrScore ?? a.score ?? 0;
|
||||
bValue = b.sonarrScore ?? b.score ?? 0;
|
||||
break;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (aValue < bValue) return sortDirection === 'asc' ? -1 : 1;
|
||||
if (aValue > bValue) return sortDirection === 'asc' ? 1 : -1;
|
||||
return 0;
|
||||
});
|
||||
|
||||
return sorted;
|
||||
}, [formats, sortColumn, sortDirection]);
|
||||
|
||||
const SortIcon = ({ column }) => {
|
||||
if (sortColumn !== column) {
|
||||
return <ChevronsUpDown className="w-3 h-3 text-gray-400" />;
|
||||
}
|
||||
return sortDirection === 'asc'
|
||||
? <ChevronUp className="w-3 h-3 text-gray-600 dark:text-gray-300" />
|
||||
: <ChevronDown className="w-3 h-3 text-gray-600 dark:text-gray-300" />;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="mb-6">
|
||||
{/* Group Header */}
|
||||
@@ -70,19 +126,37 @@ const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, i
|
||||
<table className="w-full">
|
||||
<thead>
|
||||
<tr className="border-b border-gray-200 dark:border-gray-700">
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-1/2">
|
||||
Format
|
||||
<th
|
||||
className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider w-1/2 cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
onClick={() => handleSort('name')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Format</span>
|
||||
<SortIcon column="name" />
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Radarr
|
||||
<th
|
||||
className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
onClick={() => handleSort('radarr')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Radarr</span>
|
||||
<SortIcon column="radarr" />
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">
|
||||
Sonarr
|
||||
<th
|
||||
className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer hover:bg-gray-50 dark:hover:bg-gray-700 transition-colors"
|
||||
onClick={() => handleSort('sonarr')}
|
||||
>
|
||||
<div className="flex items-center gap-1">
|
||||
<span>Sonarr</span>
|
||||
<SortIcon column="sonarr" />
|
||||
</div>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody className="divide-y divide-gray-200 dark:divide-gray-700">
|
||||
{formats.map((format) => {
|
||||
{sortedFormats.map((format) => {
|
||||
const isActive = Boolean(format.radarr) || Boolean(format.sonarr);
|
||||
const radarrScore = format.radarrScore ?? format.score ?? 0;
|
||||
const sonarrScore = format.sonarrScore ?? format.score ?? 0;
|
||||
|
||||
@@ -205,31 +205,81 @@ const FormatSettings = ({ formats, onScoreChange, onFormatToggle, activeApp }) =
|
||||
}
|
||||
});
|
||||
|
||||
// Filter grouped formats based on search
|
||||
// Filter grouped formats based on search and special filters
|
||||
const filteredGroupedFormats = useMemo(() => {
|
||||
if (searchTerms.length === 0 && !currentInput) {
|
||||
// Only use committed search terms, not currentInput
|
||||
if (searchTerms.length === 0) {
|
||||
return groupedFormats;
|
||||
}
|
||||
|
||||
// Separate special filters from regular search terms
|
||||
const specialFilters = {};
|
||||
const regularTerms = [];
|
||||
|
||||
searchTerms.forEach(term => {
|
||||
if (!term) return;
|
||||
const match = term.match(/^(enabled|radarr|sonarr):(true|false)$/i);
|
||||
if (match) {
|
||||
const [, filterType, filterValue] = match;
|
||||
specialFilters[filterType.toLowerCase()] = filterValue.toLowerCase() === 'true';
|
||||
} else {
|
||||
regularTerms.push(term);
|
||||
}
|
||||
});
|
||||
|
||||
// Start with all formats
|
||||
let filtered = allGroupedFormats;
|
||||
|
||||
// Apply special filters
|
||||
if (Object.keys(specialFilters).length > 0) {
|
||||
filtered = filtered.filter(format => {
|
||||
// Check enabled filter
|
||||
if ('enabled' in specialFilters) {
|
||||
const isEnabled = format.radarr || format.sonarr;
|
||||
if (specialFilters.enabled !== isEnabled) return false;
|
||||
}
|
||||
// Check radarr filter
|
||||
if ('radarr' in specialFilters) {
|
||||
if (specialFilters.radarr !== Boolean(format.radarr)) return false;
|
||||
}
|
||||
// Check sonarr filter
|
||||
if ('sonarr' in specialFilters) {
|
||||
if (specialFilters.sonarr !== Boolean(format.sonarr)) return false;
|
||||
}
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
// Apply regular text search
|
||||
if (regularTerms.length > 0) {
|
||||
filtered = filtered.filter(format => {
|
||||
const searchableText = format.name.toLowerCase();
|
||||
return regularTerms.every(term =>
|
||||
searchableText.includes(term.toLowerCase())
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
// Rebuild groups with filtered formats
|
||||
const filteredGroups = {};
|
||||
const searchIds = new Set(searchFilteredFormats.map(f => f.id));
|
||||
const filteredIds = new Set(filtered.map(f => f.id));
|
||||
|
||||
Object.entries(groupedFormats).forEach(([groupName, formats]) => {
|
||||
const filteredFormats = formats.filter(f => searchIds.has(f.id));
|
||||
if (filteredFormats.length > 0) {
|
||||
filteredGroups[groupName] = filteredFormats;
|
||||
const groupFiltered = formats.filter(f => filteredIds.has(f.id));
|
||||
if (groupFiltered.length > 0) {
|
||||
filteredGroups[groupName] = groupFiltered;
|
||||
}
|
||||
});
|
||||
|
||||
return filteredGroups;
|
||||
}, [groupedFormats, searchFilteredFormats, searchTerms, currentInput]);
|
||||
}, [groupedFormats, allGroupedFormats, searchTerms]);
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2">
|
||||
<SearchBar
|
||||
className="flex-1"
|
||||
placeholder="Search formats..."
|
||||
placeholder="Search formats... (try: enabled:true, radarr:true, sonarr:false)"
|
||||
searchTerms={searchTerms}
|
||||
currentInput={currentInput}
|
||||
onInputChange={setCurrentInput}
|
||||
|
||||
Reference in New Issue
Block a user