feat: enhance DataTable component with row numbering and selection features

This commit is contained in:
Yoga Pangestu 2026-04-14 21:55:46 +07:00
parent c0ea08f1e9
commit 1613cdfc09

View File

@ -19,6 +19,7 @@ import {
TableHeader, TableHeader,
TableRow, TableRow,
} from "@/components/ui/table"; } from "@/components/ui/table";
import { Checkbox } from "@/components/ui/checkbox";
import { Button } from "./ui/button"; import { Button } from "./ui/button";
import { import {
DropdownMenu, DropdownMenu,
@ -34,7 +35,7 @@ import {
} from "@/components/ui/dropdown-menu"; } from "@/components/ui/dropdown-menu";
import React from "react"; import React from "react";
import { Input } from "./ui/input"; import { Input } from "./ui/input";
import { Settings2, ChevronDown, ListFilter, Trash2 } from "lucide-react"; import { Settings2, ChevronDown, ListFilter, Trash2, ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from "lucide-react";
interface DataTableFilterOption { interface DataTableFilterOption {
columnId: string; columnId: string;
@ -55,6 +56,8 @@ interface DataTableProps<TData, TValue> {
searchKey?: string; searchKey?: string;
filters?: DataTableFilterOption[]; filters?: DataTableFilterOption[];
bulkActions?: DataTableBulkAction<TData>[]; bulkActions?: DataTableBulkAction<TData>[];
showNumbering?: boolean;
showSelection?: boolean;
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
@ -62,6 +65,8 @@ export function DataTable<TData, TValue>({
data, data,
filters, filters,
bulkActions, bulkActions,
showNumbering = true,
showSelection = true,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]); const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]); const [columnFilters, setColumnFilters] = React.useState<ColumnFiltersState>([]);
@ -69,9 +74,55 @@ export function DataTable<TData, TValue>({
const [rowSelection, setRowSelection] = React.useState({}); const [rowSelection, setRowSelection] = React.useState({});
const [globalFilter, setGlobalFilter] = React.useState(""); const [globalFilter, setGlobalFilter] = React.useState("");
const finalColumns = React.useMemo(() => {
const computedColumns = [...columns];
if (showNumbering) {
const numberingColumn: ColumnDef<TData, TValue> = {
id: "numbering",
header: "No.",
cell: ({ row, table }) => {
const pageIndex = table.getState().pagination.pageIndex;
const pageSize = table.getState().pagination.pageSize;
return pageIndex * pageSize + row.index + 1;
},
enableHiding: false,
};
computedColumns.unshift(numberingColumn);
}
if (showSelection) {
const selectColumn: ColumnDef<TData, TValue> = {
id: "select",
header: ({ table }) => (
<Checkbox
checked={
table.getIsAllPageRowsSelected() ||
(table.getIsSomePageRowsSelected() && "indeterminate")
}
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
aria-label="Select all"
/>
),
cell: ({ row }) => (
<Checkbox
checked={row.getIsSelected()}
onCheckedChange={(value) => row.toggleSelected(!!value)}
aria-label="Select row"
/>
),
enableSorting: false,
enableHiding: false,
};
computedColumns.unshift(selectColumn);
}
return computedColumns;
}, [columns, showNumbering, showSelection]);
const table = useReactTable({ const table = useReactTable({
data, data,
columns, columns: finalColumns,
getCoreRowModel: getCoreRowModel(), getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(), getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting, onSortingChange: setSorting,
@ -214,7 +265,14 @@ export function DataTable<TData, TValue>({
<TableRow key={headerGroup.id}> <TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => { {headerGroup.headers.map((header) => {
return ( return (
<TableHead key={header.id}> <TableHead
key={header.id}
className={
header.column.id === "select" || header.column.id === "numbering"
? "w-10 text-center"
: ""
}
>
{header.isPlaceholder {header.isPlaceholder
? null ? null
: flexRender(header.column.columnDef.header, header.getContext())} : flexRender(header.column.columnDef.header, header.getContext())}
@ -229,7 +287,14 @@ export function DataTable<TData, TValue>({
table.getRowModel().rows.map((row) => ( table.getRowModel().rows.map((row) => (
<TableRow key={row.id} data-state={row.getIsSelected() && "selected"}> <TableRow key={row.id} data-state={row.getIsSelected() && "selected"}>
{row.getVisibleCells().map((cell) => ( {row.getVisibleCells().map((cell) => (
<TableCell key={cell.id}> <TableCell
key={cell.id}
className={
cell.column.id === "select" || cell.column.id === "numbering"
? "w-10 text-center"
: ""
}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())} {flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell> </TableCell>
))} ))}
@ -237,7 +302,7 @@ export function DataTable<TData, TValue>({
)) ))
) : ( ) : (
<TableRow> <TableRow>
<TableCell colSpan={columns.length} className="h-24 text-center"> <TableCell colSpan={finalColumns.length} className="h-24 text-center">
Tidak ada data yang ditemukan. Tidak ada data yang ditemukan.
</TableCell> </TableCell>
</TableRow> </TableRow>
@ -247,27 +312,60 @@ export function DataTable<TData, TValue>({
</div> </div>
<div className="flex items-center justify-between py-4"> <div className="flex items-center justify-between py-4">
<div className="text-sm text-muted-foreground"> <div className="flex-1 text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} dari{" "} {table.getFilteredSelectedRowModel().rows.length} dari{" "}
{table.getFilteredRowModel().rows.length} baris dipilih. {table.getFilteredRowModel().rows.length} baris dipilih.
</div> </div>
<div className="flex items-center space-x-2"> <div className="flex items-center space-x-6 lg:space-x-8">
<Button <div className="flex items-center justify-center text-sm font-medium">
variant="outline" {table.getFilteredRowModel().rows.length > 0 ? (
size="sm" <>
onClick={() => table.previousPage()} Menampilkan {table.getState().pagination.pageIndex * table.getState().pagination.pageSize + 1} sampai{" "}
disabled={!table.getCanPreviousPage()} {Math.min((table.getState().pagination.pageIndex + 1) * table.getState().pagination.pageSize, table.getFilteredRowModel().rows.length)}{" "}
> dari {table.getFilteredRowModel().rows.length} data
Previous </>
</Button> ) : (
<Button "Tidak ada data"
variant="outline" )}
size="sm" </div>
onClick={() => table.nextPage()} <div className="flex items-center space-x-2">
disabled={!table.getCanNextPage()} <Button
> variant="outline"
Next className="hidden h-8 w-8 p-0 lg:flex"
</Button> onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Halaman pertama</span>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">Halaman sebelumnya</span>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Halaman selanjutnya</span>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">Halaman terakhir</span>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div> </div>
</div> </div>
</div> </div>