feat: enhance DataTable component with pagination support and meta data handling

This commit is contained in:
Yoga Pangestu 2026-04-30 20:06:11 +07:00
parent 4df6c215ad
commit f5698ee47f
3 changed files with 169 additions and 70 deletions

View File

@ -1,9 +1,9 @@
import type { import type {
ColumnDef, ColumnDef,
ColumnFiltersState, ColumnFiltersState,
PaginationState,
SortingState, SortingState,
VisibilityState, VisibilityState,
PaginationState,
} from '@tanstack/react-table'; } from '@tanstack/react-table';
import { import {
flexRender, flexRender,
@ -87,6 +87,7 @@ interface DataTableProps<TData, TValue> {
showSelection?: boolean; showSelection?: boolean;
rowSelection?: any; rowSelection?: any;
onRowSelectionChange?: (selection: any) => void; onRowSelectionChange?: (selection: any) => void;
meta?: any; // PaginatedData<TData> from Inertia
} }
export function DataTable<TData, TValue>({ export function DataTable<TData, TValue>({
@ -98,6 +99,7 @@ export function DataTable<TData, TValue>({
showSelection = true, showSelection = true,
rowSelection = {}, rowSelection = {},
onRowSelectionChange, onRowSelectionChange,
meta,
}: DataTableProps<TData, TValue>) { }: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]); const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] = const [columnFilters, setColumnFilters] =
@ -182,6 +184,7 @@ export function DataTable<TData, TValue>({
}, },
onPaginationChange: setPagination, onPaginationChange: setPagination,
autoResetPageIndex: false, autoResetPageIndex: false,
manualPagination: !!meta,
}); });
const selectedRows = table.getFilteredSelectedRowModel().rows; const selectedRows = table.getFilteredSelectedRowModel().rows;
@ -499,38 +502,50 @@ export function DataTable<TData, TValue>({
<div className="flex flex-col items-center justify-between gap-4 py-4 sm:flex-row"> <div className="flex flex-col items-center justify-between gap-4 py-4 sm:flex-row">
<div className="text-sm text-muted-foreground"> <div className="text-sm text-muted-foreground">
{table.getFilteredSelectedRowModel().rows.length} dari{' '} {table.getFilteredSelectedRowModel().rows.length} dari{' '}
{table.getFilteredRowModel().rows.length} baris dipilih. {meta
? meta.total
: table.getFilteredRowModel().rows.length}{' '}
baris dipilih.
</div> </div>
<div className="flex flex-col items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8"> <div className="flex flex-col items-center gap-4 sm:flex-row sm:gap-6 lg:gap-8">
<div className="flex items-center gap-2"> {!meta && (
<p className="text-sm font-medium">Baris per halaman</p> <div className="flex items-center gap-2">
<Select <p className="text-sm font-medium">
value={`${table.getState().pagination.pageSize}`} Baris per halaman
onValueChange={(value) => { </p>
table.setPageSize(Number(value)); <Select
}} value={`${table.getState().pagination.pageSize}`}
> onValueChange={(value) => {
<SelectTrigger className="h-8 w-[70px]"> table.setPageSize(Number(value));
<SelectValue }}
placeholder={ >
table.getState().pagination.pageSize <SelectTrigger className="h-8 w-[70px]">
} <SelectValue
/> placeholder={
</SelectTrigger> table.getState().pagination.pageSize
<SelectContent side="top"> }
{[10, 20, 30, 40, 50].map((pageSize) => ( />
<SelectItem </SelectTrigger>
key={pageSize} <SelectContent side="top">
value={`${pageSize}`} {[10, 20, 30, 40, 50].map((pageSize) => (
> <SelectItem
{pageSize} key={pageSize}
</SelectItem> value={`${pageSize}`}
))} >
</SelectContent> {pageSize}
</Select> </SelectItem>
</div> ))}
</SelectContent>
</Select>
</div>
)}
<div className="flex items-center justify-center text-sm font-medium"> <div className="flex items-center justify-center text-sm font-medium">
{table.getFilteredRowModel().rows.length > 0 ? ( {meta ? (
<>
Menampilkan {meta.from || 0} sampai{' '}
{meta.to || 0} dari {meta.total} data
</>
) : table.getFilteredRowModel().rows.length > 0 ? (
<> <>
Menampilkan{' '} Menampilkan{' '}
{table.getState().pagination.pageIndex * {table.getState().pagination.pageIndex *
@ -550,46 +565,114 @@ export function DataTable<TData, TValue>({
'Tidak ada data' 'Tidak ada data'
)} )}
</div> </div>
<div className="flex items-center space-x-2"> {meta ? (() => {
<Button const { current_page, last_page, prev_page_url, next_page_url } = meta;
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex" const leftPages = [];
onClick={() => table.setPageIndex(0)} for (let i = current_page - 3; i < current_page; i++) {
disabled={!table.getCanPreviousPage()} if (i > 0) leftPages.push(i);
> }
<span className="sr-only">Halaman pertama</span>
<ChevronsLeft className="h-4 w-4" /> const rightPages = [];
</Button> for (let i = current_page + 1; i <= current_page + 2; i++) {
<Button if (i <= last_page) rightPages.push(i);
variant="outline" }
className="h-8 w-8 p-0"
onClick={() => table.previousPage()} const handlePageChange = (page: number) => {
disabled={!table.getCanPreviousPage()} const url = new URL(window.location.href);
> url.searchParams.set('page', page.toString());
<span className="sr-only">Halaman sebelumnya</span> import('@inertiajs/react').then(({ router }) => {
<ChevronLeft className="h-4 w-4" /> router.get(url.toString(), {}, { preserveState: true, preserveScroll: true });
</Button> });
<Button };
variant="outline"
className="h-8 w-8 p-0" return (
onClick={() => table.nextPage()} <div className="flex items-center space-x-2">
disabled={!table.getCanNextPage()} <Button
> variant="outline"
<span className="sr-only">Halaman selanjutnya</span> className="h-8 w-8 p-0"
<ChevronRight className="h-4 w-4" /> onClick={() => {
</Button> if (prev_page_url) {
<Button import('@inertiajs/react').then(({ router }) => {
variant="outline" router.get(prev_page_url, {}, { preserveState: true, preserveScroll: true });
className="hidden h-8 w-8 p-0 lg:flex" });
onClick={() => }
table.setPageIndex(table.getPageCount() - 1) }}
} disabled={!prev_page_url}
disabled={!table.getCanNextPage()} >
> <ChevronLeft className="h-4 w-4" />
<span className="sr-only">Halaman terakhir</span> </Button>
<ChevronsRight className="h-4 w-4" />
</Button> {leftPages.map(p => (
</div> <Button key={p} variant="outline" className="h-8 px-3" onClick={() => handlePageChange(p)}>
{p}
</Button>
))}
<Button variant="outline" className="h-8 px-3 border-none bg-transparent shadow-none" disabled>
...
</Button>
{rightPages.map(p => (
<Button key={p} variant="outline" className="h-8 px-3" onClick={() => handlePageChange(p)}>
{p}
</Button>
))}
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => {
if (next_page_url) {
import('@inertiajs/react').then(({ router }) => {
router.get(next_page_url, {}, { preserveState: true, preserveScroll: true });
});
}
}}
disabled={!next_page_url}
>
<ChevronRight className="h-4 w-4" />
</Button>
</div>
);
})() : (
<div className="flex items-center space-x-2">
<Button
variant="outline"
className="hidden h-8 w-8 p-0 lg:flex"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
className="h-8 w-8 p-0"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<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()}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
)}
</div> </div>
</div> </div>
</div> </div>

View File

@ -12,3 +12,4 @@ export type * from './system-log';
export type * from './dashboard'; export type * from './dashboard';
export type * from './role'; export type * from './role';
export type * from './permission'; export type * from './permission';
export type * from './pagination';

View File

@ -0,0 +1,15 @@
export interface PaginatedData<T> {
current_page: number;
data: T[];
first_page_url: string;
from: number;
last_page: number;
last_page_url: string;
links: { url: string | null; label: string; active: boolean }[];
next_page_url: string | null;
path: string;
per_page: number;
prev_page_url: string | null;
to: number;
total: number;
}