Parcourir la source

feat(dashboard): Implement data table sorting

Michael Bromley il y a 10 mois
Parent
commit
21b2ad099f

+ 50 - 0
packages/dashboard/src/framework/internal/data-table/data-table-view-options.tsx

@@ -0,0 +1,50 @@
+'use client';
+
+import { DropdownMenuTrigger } from '@radix-ui/react-dropdown-menu';
+import { Table } from '@tanstack/react-table';
+import { Settings2 } from 'lucide-react';
+
+import { Button } from '@/components/ui/button';
+import {
+    DropdownMenu,
+    DropdownMenuCheckboxItem,
+    DropdownMenuContent,
+    DropdownMenuLabel,
+    DropdownMenuSeparator,
+} from '@/components/ui/dropdown-menu';
+
+interface DataTableViewOptionsProps<TData> {
+    table: Table<TData>;
+}
+
+export function DataTableViewOptions<TData>({ table }: DataTableViewOptionsProps<TData>) {
+    return (
+        <DropdownMenu>
+            <DropdownMenuTrigger asChild>
+                <Button variant="outline" size="sm" className="ml-auto hidden h-8 lg:flex">
+                    <Settings2 />
+                    View
+                </Button>
+            </DropdownMenuTrigger>
+            <DropdownMenuContent align="end" className="w-[150px]">
+                <DropdownMenuLabel>Toggle columns</DropdownMenuLabel>
+                <DropdownMenuSeparator />
+                {table
+                    .getAllColumns()
+                    .filter(column => typeof column.accessorFn !== 'undefined' && column.getCanHide())
+                    .map(column => {
+                        return (
+                            <DropdownMenuCheckboxItem
+                                key={column.id}
+                                className="capitalize"
+                                checked={column.getIsVisible()}
+                                onCheckedChange={value => column.toggleVisibility(!!value)}
+                            >
+                                {column.id}
+                            </DropdownMenuCheckboxItem>
+                        );
+                    })}
+            </DropdownMenuContent>
+        </DropdownMenu>
+    );
+}

+ 14 - 1
packages/dashboard/src/framework/internal/data-table/data-table.tsx

@@ -2,6 +2,7 @@
 
 import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
 import { DataTablePagination } from '@/framework/internal/data-table/data-table-pagination.js';
+import { DataTableViewOptions } from '@/framework/internal/data-table/data-table-view-options.js';
 
 import {
     ColumnDef,
@@ -9,6 +10,7 @@ import {
     getCoreRowModel,
     getPaginationRowModel,
     PaginationState,
+    SortingState,
     useReactTable,
 } from '@tanstack/react-table';
 import React, { useEffect } from 'react';
@@ -20,6 +22,7 @@ interface DataTableProps<TData, TValue> {
     page?: number;
     itemsPerPage?: number;
     onPageChange?: (page: number, itemsPerPage: number) => void;
+    onSortChange?: (sorting: SortingState) => void;
 }
 
 export function DataTable<TData, TValue>({
@@ -29,7 +32,9 @@ export function DataTable<TData, TValue>({
     page,
     itemsPerPage,
     onPageChange,
+    onSortChange,
 }: DataTableProps<TData, TValue>) {
+    const [sorting, setSorting] = React.useState<SortingState>([]);
     const [pagination, setPagination] = React.useState<PaginationState>({
         pageIndex: (page ?? 1) - 1,
         pageSize: itemsPerPage ?? 10,
@@ -40,10 +45,13 @@ export function DataTable<TData, TValue>({
         getCoreRowModel: getCoreRowModel(),
         getPaginationRowModel: getPaginationRowModel(),
         manualPagination: true,
+        manualSorting: true,
         rowCount: totalItems,
         onPaginationChange: setPagination,
+        onSortingChange: setSorting,
         state: {
             pagination,
+            sorting,
         },
     });
 
@@ -51,9 +59,14 @@ export function DataTable<TData, TValue>({
         onPageChange?.(pagination.pageIndex + 1, pagination.pageSize);
     }, [pagination]);
 
+    useEffect(() => {
+        onSortChange?.(sorting);
+    }, [sorting]);
+
     return (
         <>
-            <div className="rounded-md border">
+            <DataTableViewOptions table={table} />
+            <div className="rounded-md border my-2">
                 <Table>
                     <TableHeader>
                         {table.getHeaderGroups().map(headerGroup => (

+ 35 - 2
packages/dashboard/src/framework/internal/page/list-page.tsx

@@ -1,3 +1,4 @@
+import { Button } from '@/components/ui/button.js';
 import { useComponentRegistry } from '@/framework/internal/component-registry/component-registry.js';
 import { DataTable } from '@/framework/internal/data-table/data-table.js';
 import {
@@ -13,6 +14,7 @@ import { AnyRouter, Route, useNavigate } from '@tanstack/react-router';
 import { AnyRoute } from '@tanstack/react-router';
 import { createColumnHelper } from '@tanstack/react-table';
 import { ResultOf } from 'gql.tada';
+import { ArrowDown, ArrowUp, ArrowUpDown } from 'lucide-react';
 import React from 'react';
 
 type ListQueryFields<T extends TypedDocumentNode> = {
@@ -56,15 +58,24 @@ export function ListPage<T extends TypedDocumentNode<U>, U extends Record<string
         page: routeSearch.page ? parseInt(routeSearch.page) : 1,
         itemsPerPage: routeSearch.perPage ? parseInt(routeSearch.perPage) : 10,
     };
+    const sorting = routeSearch.sort;
+    const sort = (sorting?.split(',') || [])?.reduce((acc: any, sort: string) => {
+        const [field, direction] = sort.split(':');
+        if (!field || !direction) {
+            return acc;
+        }
+        return { ...acc, [field]: direction === '1' ? 'ASC' : 'DESC' };
+    }, {});
     const { data } = useQuery({
         queryFn: () =>
             api.query(listQuery, {
                 options: {
                     take: pagination.itemsPerPage,
                     skip: (pagination.page - 1) * pagination.itemsPerPage,
+                    sort,
                 },
             }),
-        queryKey: ['ListPage', route.id, pagination],
+        queryKey: ['ListPage', route.id, pagination, sorting],
     });
     const fields = getListQueryFields(listQuery);
     const queryName = getQueryName(listQuery);
@@ -72,7 +83,6 @@ export function ListPage<T extends TypedDocumentNode<U>, U extends Record<string
 
     const columns = fields.map(field =>
         columnHelper.accessor(field.name as any, {
-            header: customizeColumns?.[field.name as keyof ListQueryFields<T>]?.header ?? field.name,
             meta: { type: field.type },
             cell: ({ cell }) => {
                 const value = cell.getValue();
@@ -92,6 +102,22 @@ export function ListPage<T extends TypedDocumentNode<U>, U extends Record<string
                 }
                 return value;
             },
+            header: ({ column }) => {
+                const columSort = column.getIsSorted();
+                const nextSort = columSort === 'asc' ? true : columSort === 'desc' ? undefined : false;
+                return (
+                    <Button variant="ghost" onClick={() => column.toggleSorting(nextSort)}>
+                        {customizeColumns?.[field.name as keyof ListQueryFields<T>]?.header ?? field.name}
+                        {columSort === 'desc' ? (
+                            <ArrowUp />
+                        ) : columSort === 'asc' ? (
+                            <ArrowDown />
+                        ) : (
+                            <ArrowUpDown className="opacity-30" />
+                        )}
+                    </Button>
+                );
+            },
         }),
     );
 
@@ -107,6 +133,13 @@ export function ListPage<T extends TypedDocumentNode<U>, U extends Record<string
                 onPageChange={(page, perPage) => {
                     navigate({ search: () => ({ page, perPage }) as never });
                 }}
+                onSortChange={sorting => {
+                    navigate({
+                        replace: false,
+                        search: () =>
+                            ({ sort: sorting.map(s => `${s.id}:${s.desc ? 1 : 0}`).join(',') }) as never,
+                    });
+                }}
             ></DataTable>
         </div>
     );

+ 4 - 1
packages/dashboard/src/routes/__root.tsx

@@ -1,5 +1,5 @@
 import { AuthContext } from '@/auth.js';
-import { createRootRouteWithContext, Outlet } from '@tanstack/react-router';
+import { createRootRouteWithContext, Outlet, retainSearchParams } from '@tanstack/react-router';
 import { TanStackRouterDevtools } from '@tanstack/router-devtools';
 import * as React from 'react';
 
@@ -9,6 +9,9 @@ interface MyRouterContext {
 
 export const Route = createRootRouteWithContext<MyRouterContext>()({
     component: RootComponent,
+    search: {
+        middlewares: [retainSearchParams(['page', 'perPage', 'sort'] as any)],
+    },
 });
 
 function RootComponent() {