feat(scoring): implement sorting and filtering for format groups

This commit is contained in:
Sam Chau
2025-08-14 04:21:42 +09:30
parent 75eef53b40
commit 9b3e261ec2
2 changed files with 141 additions and 17 deletions

View File

@@ -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 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 NumberInput from '@ui/NumberInput';
import Tooltip from '@ui/Tooltip'; import Tooltip from '@ui/Tooltip';
import { Copy } from 'lucide-react'; import { Copy } from 'lucide-react';
const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, icon }) => { const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, icon }) => {
const [isExpanded, setIsExpanded] = useState(true); const [isExpanded, setIsExpanded] = useState(true);
const [sortColumn, setSortColumn] = useState('radarr');
const [sortDirection, setSortDirection] = useState('desc');
// Map group names to icons // Map group names to icons
const groupIcons = { const groupIcons = {
@@ -45,6 +47,60 @@ const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, i
} }
}, [formats, onScoreChange]); }, [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 ( return (
<div className="mb-6"> <div className="mb-6">
{/* Group Header */} {/* Group Header */}
@@ -70,19 +126,37 @@ const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, i
<table className="w-full"> <table className="w-full">
<thead> <thead>
<tr className="border-b border-gray-200 dark:border-gray-700"> <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"> <th
Format 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>
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th
Radarr 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>
<th className="text-left px-4 py-3 text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider"> <th
Sonarr 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> </th>
</tr> </tr>
</thead> </thead>
<tbody className="divide-y divide-gray-200 dark:divide-gray-700"> <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 isActive = Boolean(format.radarr) || Boolean(format.sonarr);
const radarrScore = format.radarrScore ?? format.score ?? 0; const radarrScore = format.radarrScore ?? format.score ?? 0;
const sonarrScore = format.sonarrScore ?? format.score ?? 0; const sonarrScore = format.sonarrScore ?? format.score ?? 0;

View File

@@ -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(() => { const filteredGroupedFormats = useMemo(() => {
if (searchTerms.length === 0 && !currentInput) { // Only use committed search terms, not currentInput
if (searchTerms.length === 0) {
return groupedFormats; 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 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]) => { Object.entries(groupedFormats).forEach(([groupName, formats]) => {
const filteredFormats = formats.filter(f => searchIds.has(f.id)); const groupFiltered = formats.filter(f => filteredIds.has(f.id));
if (filteredFormats.length > 0) { if (groupFiltered.length > 0) {
filteredGroups[groupName] = filteredFormats; filteredGroups[groupName] = groupFiltered;
} }
}); });
return filteredGroups; return filteredGroups;
}, [groupedFormats, searchFilteredFormats, searchTerms, currentInput]); }, [groupedFormats, allGroupedFormats, searchTerms]);
return ( return (
<div className="space-y-4"> <div className="space-y-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<SearchBar <SearchBar
className="flex-1" className="flex-1"
placeholder="Search formats..." placeholder="Search formats... (try: enabled:true, radarr:true, sonarr:false)"
searchTerms={searchTerms} searchTerms={searchTerms}
currentInput={currentInput} currentInput={currentInput}
onInputChange={setCurrentInput} onInputChange={setCurrentInput}