From a558b46b0eee3d524036b80b530892d8ae4f9ea6 Mon Sep 17 00:00:00 2001 From: Nicolas Meienberger Date: Sat, 2 May 2026 10:01:53 +0200 Subject: [PATCH] feat: make notifications sortable --- .../notifications/routes/notifications.tsx | 153 +++++++++++++----- 1 file changed, 112 insertions(+), 41 deletions(-) diff --git a/app/client/modules/notifications/routes/notifications.tsx b/app/client/modules/notifications/routes/notifications.tsx index 836a7d41..8a8744c4 100644 --- a/app/client/modules/notifications/routes/notifications.tsx +++ b/app/client/modules/notifications/routes/notifications.tsx @@ -1,6 +1,18 @@ import { useSuspenseQuery } from "@tanstack/react-query"; +import { + flexRender, + getCoreRowModel, + getFilteredRowModel, + getSortedRowModel, + type ColumnDef, + type ColumnFiltersState, + type SortingState, + useReactTable, +} from "@tanstack/react-table"; import { Bell, Plus, RotateCcw } from "lucide-react"; import { useState } from "react"; +import { listNotificationDestinationsOptions } from "~/client/api-client/@tanstack/react-query.gen"; +import { DataTableSortHeader } from "~/client/components/data-table-sort-header"; import { EmptyState } from "~/client/components/empty-state"; import { StatusDot } from "~/client/components/status-dot"; import { Button } from "~/client/components/ui/button"; @@ -8,20 +20,47 @@ import { Card } from "~/client/components/ui/card"; import { Input } from "~/client/components/ui/input"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "~/client/components/ui/select"; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "~/client/components/ui/table"; -import { listNotificationDestinationsOptions } from "~/client/api-client/@tanstack/react-query.gen"; import { useNavigate } from "@tanstack/react-router"; import { cn } from "~/client/lib/utils"; -export function NotificationsPage() { - const [searchQuery, setSearchQuery] = useState(""); - const [typeFilter, setTypeFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(""); +type NotificationRow = { + id: number; + name: string; + type: "email" | "slack" | "discord" | "gotify" | "ntfy" | "pushover" | "telegram" | "custom" | "generic"; + enabled: boolean; +}; - const clearFilters = () => { - setSearchQuery(""); - setTypeFilter(""); - setStatusFilter(""); - }; +const notificationColumns: ColumnDef[] = [ + { + accessorKey: "name", + header: ({ column }) => , + cell: ({ row }) => row.original.name, + }, + { + accessorKey: "type", + header: ({ column }) => , + cell: ({ row }) => row.original.type, + filterFn: (row, id, value) => row.getValue(id) === value, + }, + { + accessorFn: (row) => (row.enabled ? "enabled" : "disabled"), + id: "status", + header: ({ column }) => ( + + ), + cell: ({ row }) => ( + + ), + filterFn: (row, id, value) => row.getValue(id) === value, + }, +]; + +export function NotificationsPage() { + const [columnFilters, setColumnFilters] = useState([]); + const [sorting, setSorting] = useState([]); const navigate = useNavigate(); @@ -29,15 +68,24 @@ export function NotificationsPage() { ...listNotificationDestinationsOptions(), }); - const filteredNotifications = data.filter((notification) => { - const matchesSearch = notification.name.toLowerCase().includes(searchQuery.toLowerCase()); - const matchesType = !typeFilter || notification.type === typeFilter; - const matchesStatus = !statusFilter || (statusFilter === "enabled" ? notification.enabled : !notification.enabled); - return matchesSearch && matchesType && matchesStatus; + const table = useReactTable({ + data, + columns: notificationColumns, + state: { columnFilters, sorting }, + onColumnFiltersChange: setColumnFilters, + onSortingChange: setSorting, + getCoreRowModel: getCoreRowModel(), + getFilteredRowModel: getFilteredRowModel(), + getSortedRowModel: getSortedRowModel(), }); + const rows = table.getRowModel().rows; + const hasFilters = columnFilters.length > 0; + + const clearFilters = () => table.resetColumnFilters(); + const hasNoNotifications = data.length === 0; - const hasNoFilteredNotifications = filteredNotifications.length === 0 && !hasNoNotifications; + const hasNoFilteredNotifications = rows.length === 0 && !hasNoNotifications; if (hasNoNotifications) { return ( @@ -55,6 +103,10 @@ export function NotificationsPage() { ); } + const search = (table.getColumn("name")?.getFilterValue() as string) ?? ""; + const type = (table.getColumn("type")?.getFilterValue() as string) ?? ""; + const status = (table.getColumn("status")?.getFilterValue() as string) ?? ""; + return (
@@ -62,10 +114,10 @@ export function NotificationsPage() { setSearchQuery(e.target.value)} + value={search} + onChange={(e) => table.getColumn("name")?.setFilterValue(e.target.value)} /> - table.getColumn("type")?.setFilterValue(value)}> @@ -80,7 +132,7 @@ export function NotificationsPage() { Custom - table.getColumn("status")?.setFilterValue(value)}> @@ -89,7 +141,7 @@ export function NotificationsPage() { Disabled - {(searchQuery || typeFilter || statusFilter) && ( + {hasFilters && (