diff --git a/frontend/src/components/profile/scoring/FormatGroup.jsx b/frontend/src/components/profile/scoring/FormatGroup.jsx index d8dcf85..52e7ee3 100644 --- a/frontend/src/components/profile/scoring/FormatGroup.jsx +++ b/frontend/src/components/profile/scoring/FormatGroup.jsx @@ -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 ; + } + return sortDirection === 'asc' + ? + : ; + }; + return (
{/* Group Header */} @@ -70,19 +126,37 @@ const FormatGroup = memo(({ groupName, formats, onScoreChange, onFormatToggle, i - - - - {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; diff --git a/frontend/src/components/profile/scoring/FormatSettings.jsx b/frontend/src/components/profile/scoring/FormatSettings.jsx index 09ed7e4..4cdae4b 100644 --- a/frontend/src/components/profile/scoring/FormatSettings.jsx +++ b/frontend/src/components/profile/scoring/FormatSettings.jsx @@ -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 (
- Format + handleSort('name')} + > +
+ Format + +
- Radarr + handleSort('radarr')} + > +
+ Radarr + +
- Sonarr + handleSort('sonarr')} + > +
+ Sonarr + +