diff --git a/web/src/screens/settings/DownloadClient.tsx b/web/src/screens/settings/DownloadClient.tsx index f399369..c0f75a5 100644 --- a/web/src/screens/settings/DownloadClient.tsx +++ b/web/src/screens/settings/DownloadClient.tsx @@ -8,12 +8,74 @@ import { APIClient } from "../../api/APIClient"; import { DownloadClientTypeNameMap } from "../../domain/constants"; import toast from "react-hot-toast"; import Toast from "../../components/notifications/Toast"; +import { useState, useMemo } from "react"; interface DLSettingsItemProps { client: DownloadClient; idx: number; } +interface ListItemProps { + clients: DownloadClient; +} + +interface SortConfig { + key: keyof ListItemProps["clients"] | "enabled"; + direction: "ascending" | "descending"; +} + +function useSort(items: ListItemProps["clients"][], config?: SortConfig) { + const [sortConfig, setSortConfig] = useState(config); + + + + const sortedItems = useMemo(() => { + if (!sortConfig) { + return items; + } + + const sortableItems = [...items]; + + sortableItems.sort((a, b) => { + const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string; + const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string; + + if (aValue < bValue) { + return sortConfig.direction === "ascending" ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === "ascending" ? 1 : -1; + } + return 0; + }); + + return sortableItems; + }, [items, sortConfig]); + + const requestSort = (key: keyof ListItemProps["clients"]) => { + let direction: "ascending" | "descending" = "ascending"; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "ascending" + ) { + direction = "descending"; + } + setSortConfig({ key, direction }); + }; + + + const getSortIndicator = (key: keyof ListItemProps["clients"]) => { + if (!sortConfig || sortConfig.key !== key) { + return ""; + } + + return sortConfig.direction === "ascending" ? "↑" : "↓"; + }; + + return { items: sortedItems, requestSort, sortConfig, getSortIndicator }; +} + function DownloadClientSettingsListItem({ client }: DLSettingsItemProps) { const [updateClientIsOpen, toggleUpdateClient] = useToggle(false); @@ -84,10 +146,13 @@ function DownloadClientSettings() { { refetchOnWindowFocus: false } ); + const sortedClients = useSort(data || []); + if (error) { return

Failed to fetch download clients

; } + return (
@@ -113,24 +178,33 @@ function DownloadClientSettings() {
- {data && data.length > 0 ? + {sortedClients.items.length > 0 ?
  1. -
    - Enabled +
    sortedClients.requestSort("enabled")}> + Enabled {sortedClients.getSortIndicator("enabled")}
    -
    - Name +
    sortedClients.requestSort("name")} + > + Name {sortedClients.getSortIndicator("name")}
    -
    - Host +
    sortedClients.requestSort("host")} + > + Host {sortedClients.getSortIndicator("host")}
    -
    - Type +
    sortedClients.requestSort("type")} + > + Type {sortedClients.getSortIndicator("type")}
  2. - {data && data.map((client, idx) => ( + {sortedClients.items.map((client, idx) => ( ))}
diff --git a/web/src/screens/settings/Feed.tsx b/web/src/screens/settings/Feed.tsx index c3a4fe5..6425154 100644 --- a/web/src/screens/settings/Feed.tsx +++ b/web/src/screens/settings/Feed.tsx @@ -4,7 +4,7 @@ import { APIClient } from "../../api/APIClient"; import { Menu, Switch, Transition } from "@headlessui/react"; import { baseUrl, classNames, IsEmptyDate, simplifyDate } from "../../utils"; -import { Fragment, useRef, useState } from "react"; +import { Fragment, useRef, useState, useMemo } from "react"; import { toast } from "react-hot-toast"; import Toast from "../../components/notifications/Toast"; import { queryClient } from "../../App"; @@ -20,6 +20,62 @@ import { FeedUpdateForm } from "../../forms/settings/FeedForms"; import { EmptySimple } from "../../components/emptystates"; import { ImplementationBadges } from "./Indexer"; +interface SortConfig { + key: keyof ListItemProps["feed"] | "enabled"; + direction: "ascending" | "descending"; +} + +function useSort(items: ListItemProps["feed"][], config?: SortConfig) { + const [sortConfig, setSortConfig] = useState(config); + + + + const sortedItems = useMemo(() => { + if (!sortConfig) { + return items; + } + + const sortableItems = [...items]; + + sortableItems.sort((a, b) => { + const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string; + const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string; + + if (aValue < bValue) { + return sortConfig.direction === "ascending" ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === "ascending" ? 1 : -1; + } + return 0; + }); + + return sortableItems; + }, [items, sortConfig]); + + const requestSort = (key: keyof ListItemProps["feed"] | "enabled") => { let direction: "ascending" | "descending" = "ascending"; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "ascending" + ) { + direction = "descending"; + } + setSortConfig({ key, direction }); + }; + + + const getSortIndicator = (key: keyof ListItemProps["feed"]) => { + if (!sortConfig || sortConfig.key !== key) { + return ""; + } + + return sortConfig.direction === "ascending" ? "↑" : "↓"; + }; + + return { items: sortedItems, requestSort, sortConfig, getSortIndicator }; +} + function FeedSettings() { const { data } = useQuery( "feeds", @@ -27,6 +83,8 @@ function FeedSettings() { { refetchOnWindowFocus: false } ); + const sortedFeeds = useSort(data || []); + return (
@@ -44,20 +102,28 @@ function FeedSettings() {
  1. Enabled + className="flex col-span-2 sm:col-span-1 px-4 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" + onClick={() => sortedFeeds.requestSort("enabled")}> + Enabled {sortedFeeds.getSortIndicator("enabled")}
    Name + className="col-span-6 pl-12 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" + onClick={() => sortedFeeds.requestSort("name")}> + Name {sortedFeeds.getSortIndicator("name")}
    Type + className="hidden md:flex col-span-1 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" + onClick={() => sortedFeeds.requestSort("type")}> + Type {sortedFeeds.getSortIndicator("type")}
    Last run + className="hidden md:flex col-span-3 px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider cursor-pointer" + onClick={() => sortedFeeds.requestSort("last_run")}> + Last run {sortedFeeds.getSortIndicator("last_run")}
  2. - {data && data.map((f) => ( - + {sortedFeeds.items.map((feed, idx) => ( + ))}
diff --git a/web/src/screens/settings/Indexer.tsx b/web/src/screens/settings/Indexer.tsx index c57b3d8..6e87548 100644 --- a/web/src/screens/settings/Indexer.tsx +++ b/web/src/screens/settings/Indexer.tsx @@ -6,6 +6,61 @@ import { classNames } from "../../utils"; import { EmptySimple } from "../../components/emptystates"; import { APIClient } from "../../api/APIClient"; import { componentMapType } from "../../forms/settings/DownloadClientForms"; +import { useState, useMemo } from "react"; + +interface SortConfig { + key: keyof ListItemProps["indexer"] | "enabled"; + direction: "ascending" | "descending"; +} + +function useSort(items: ListItemProps["indexer"][], config?: SortConfig) { + const [sortConfig, setSortConfig] = useState(config); + + const sortedItems = useMemo(() => { + if (!sortConfig) { + return items; + } + + const sortableItems = [...items]; + + sortableItems.sort((a, b) => { + const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string; + const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string; + + if (aValue < bValue) { + return sortConfig.direction === "ascending" ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === "ascending" ? 1 : -1; + } + return 0; + }); + + return sortableItems; + }, [items, sortConfig]); + + const requestSort = (key: keyof ListItemProps["indexer"]) => { + let direction: "ascending" | "descending" = "ascending"; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "ascending" + ) { + direction = "descending"; + } + setSortConfig({ key, direction }); + }; + + const getSortIndicator = (key: keyof ListItemProps["indexer"]) => { + if (!sortConfig || sortConfig.key !== key) { + return ""; + } + + return sortConfig.direction === "ascending" ? "↑" : "↓"; + }; + + return { items: sortedItems, requestSort, sortConfig, getSortIndicator }; +} const ImplementationBadgeIRC = () => ( @@ -100,6 +155,8 @@ function IndexerSettings() { { refetchOnWindowFocus: false } ); + const sortedIndexers = useSort(data || []); + if (error) return (

An error has occurred

); @@ -133,17 +190,26 @@ function IndexerSettings() {
  1. -
    - Enabled +
    sortedIndexers.requestSort("enabled")} + > + Enabled {sortedIndexers.getSortIndicator("enabled")}
    -
    - Name +
    sortedIndexers.requestSort("name")} + > + Name {sortedIndexers.getSortIndicator("name")}
    -
    - Implementation +
    sortedIndexers.requestSort("implementation")} + > + Implementation {sortedIndexers.getSortIndicator("implementation")}
  2. - {data.map((indexer, idx) => ( + {sortedIndexers.items.map((indexer, idx) => ( ))}
diff --git a/web/src/screens/settings/Irc.tsx b/web/src/screens/settings/Irc.tsx index 189113d..77553de 100644 --- a/web/src/screens/settings/Irc.tsx +++ b/web/src/screens/settings/Irc.tsx @@ -9,6 +9,7 @@ import { LockClosedIcon, LockOpenIcon } from "@heroicons/react/24/solid"; import { Menu, Switch, Transition } from "@headlessui/react"; import { Fragment, useRef } from "react"; import { DeleteModal } from "../../components/modals"; +import { useState, useMemo } from "react"; import { toast } from "react-hot-toast"; import Toast from "../../components/notifications/Toast"; @@ -21,6 +22,64 @@ import { TrashIcon } from "@heroicons/react/24/outline"; +interface SortConfig { + key: keyof ListItemProps["network"] | "enabled"; + direction: "ascending" | "descending"; +} + +function useSort(items: ListItemProps["network"][], config?: SortConfig) { + const [sortConfig, setSortConfig] = useState(config); + + + + const sortedItems = useMemo(() => { + if (!sortConfig) { + return items; + } + + const sortableItems = [...items]; + + sortableItems.sort((a, b) => { + const aValue = sortConfig.key === "enabled" ? (a[sortConfig.key] ?? false) as number | boolean | string : a[sortConfig.key] as number | boolean | string; + const bValue = sortConfig.key === "enabled" ? (b[sortConfig.key] ?? false) as number | boolean | string : b[sortConfig.key] as number | boolean | string; + + if (aValue < bValue) { + return sortConfig.direction === "ascending" ? -1 : 1; + } + if (aValue > bValue) { + return sortConfig.direction === "ascending" ? 1 : -1; + } + return 0; + }); + + return sortableItems; + }, [items, sortConfig]); + + const requestSort = (key: keyof ListItemProps["network"]) => { + let direction: "ascending" | "descending" = "ascending"; + if ( + sortConfig && + sortConfig.key === key && + sortConfig.direction === "ascending" + ) { + direction = "descending"; + } + setSortConfig({ key, direction }); + }; + + + const getSortIndicator = (key: keyof ListItemProps["network"]) => { + if (!sortConfig || sortConfig.key !== key) { + return ""; + } + + return sortConfig.direction === "ascending" ? "↑" : "↓"; + }; + + return { items: sortedItems, requestSort, sortConfig, getSortIndicator }; +} + + const IrcSettings = () => { const [expandNetworks, toggleExpand] = useToggle(false); const [addNetworkIsOpen, toggleAddNetwork] = useToggle(false); @@ -31,6 +90,9 @@ const IrcSettings = () => { refetchInterval: 3000 }); + const sortedNetworks = useSort(data || []); + + return (
@@ -100,21 +162,25 @@ const IrcSettings = () => {
  1. -
    - Enabled +
    sortedNetworks.requestSort("enabled")}> + Enabled {sortedNetworks.getSortIndicator("enabled")}
    -
    - Network +
    sortedNetworks.requestSort("name")}> + Network {sortedNetworks.getSortIndicator("name")}
    -
    - Server +
    sortedNetworks.requestSort("server")}> + Server {sortedNetworks.getSortIndicator("server")}
    -
    - Nick +
    sortedNetworks.requestSort("nick")}> + Nick {sortedNetworks.getSortIndicator("nick")}
  2. {data && - data.map((network, idx) => ( + sortedNetworks.items.map((network, idx) => ( ))}