mirror of
https://github.com/Dictionarry-Hub/profilarr.git
synced 2026-01-22 10:51:02 +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 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;
|
||||||
|
|||||||
@@ -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}
|
||||||
|
|||||||
Reference in New Issue
Block a user