sortedClients.requestSort("host")}
+ >
+ Host {sortedClients.getSortIndicator("host")}
- Type
+
sortedClients.requestSort("type")}
+ >
+ Type {sortedClients.getSortIndicator("type")}
- {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() {
-
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")}
- {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() {
-
-
- Enabled
+
sortedIndexers.requestSort("enabled")}
+ >
+ Enabled {sortedIndexers.getSortIndicator("enabled")}
-
- Name
+
sortedIndexers.requestSort("name")}
+ >
+ Name {sortedIndexers.getSortIndicator("name")}
-
- Implementation
+
sortedIndexers.requestSort("implementation")}
+ >
+ Implementation {sortedIndexers.getSortIndicator("implementation")}
- {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 = () => {
-
-
- 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")}
{data &&
- data.map((network, idx) => (
+ sortedNetworks.items.map((network, idx) => (
))}