dstpabuaran.com/resources/js/components/data-table.tsx

388 lines
15 KiB
TypeScript

import {
DndContext,
KeyboardSensor,
PointerSensor,
useSensor,
useSensors
} from '@dnd-kit/core';
import type { DragEndEvent } from '@dnd-kit/core';
import {
SortableContext,
useSortable,
verticalListSortingStrategy,
} from '@dnd-kit/sortable';
import { CSS } from '@dnd-kit/utilities';
import {
flexRender,
getCoreRowModel,
getFilteredRowModel,
getPaginationRowModel,
getSortedRowModel,
useReactTable
} from '@tanstack/react-table';
import type { ColumnDef, ColumnFiltersState, SortingState } from '@tanstack/react-table';
import { GripVertical } from 'lucide-react';
import { ChevronLeft, ChevronRight, ChevronsLeft, ChevronsRight } from 'lucide-react';
import * as React from 'react';
import { Button } from '@/components/ui/button';
import { Card, CardContent } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
searchKey?: string;
searchPlaceholder?: string;
emptyText?: string;
onReorder?: (items: TData[]) => void;
getRowId?: (item: TData) => string | number;
}
const DragHandleContext = React.createContext<{
listeners?: Record<string, unknown>;
attributes?: Record<string, string>;
}>({});
export function DragHandleTrigger() {
const { listeners, attributes } = React.useContext(DragHandleContext);
return (
<Button
variant="ghost"
size="icon"
className="-ml-2 cursor-grab active:cursor-grabbing"
{...listeners}
{...attributes}
>
<GripVertical className="h-4 w-4 text-muted-foreground" />
</Button>
);
}
function SortableTableRow({
id,
children,
}: {
id: string;
children: React.ReactNode;
}) {
const {
attributes,
listeners,
setNodeRef,
transform,
transition,
isDragging,
} = useSortable({ id });
const style = {
transform: CSS.Transform.toString(transform),
transition,
opacity: isDragging ? 0.4 : undefined,
};
return (
<DragHandleContext.Provider value={{ listeners, attributes }}>
<TableRow
ref={setNodeRef}
style={style}
data-state={isDragging ? 'dragging' : undefined}
>
{children}
</TableRow>
</DragHandleContext.Provider>
);
}
export function DataTable<TData, TValue>({
columns,
data,
searchKey,
searchPlaceholder = 'Cari...',
emptyText = 'Tidak ada data.',
onReorder,
getRowId,
}: DataTableProps<TData, TValue>) {
const [sorting, setSorting] = React.useState<SortingState>([]);
const [columnFilters, setColumnFilters] =
React.useState<ColumnFiltersState>([]);
const isSortable = !!onReorder && !!getRowId;
const visibleColumns = isSortable
? [
{
id: 'drag',
header: '',
cell: () => <DragHandleTrigger />,
meta: {
className: 'w-[40px]',
headerClassName: 'w-[40px]',
},
} as ColumnDef<TData, TValue>,
...columns,
]
: columns;
const sensors = useSensors(
useSensor(PointerSensor, {
activationConstraint: { distance: 8 },
}),
useSensor(KeyboardSensor),
);
const table = useReactTable({
data,
columns: visibleColumns,
getCoreRowModel: getCoreRowModel(),
getPaginationRowModel: getPaginationRowModel(),
onSortingChange: setSorting,
getSortedRowModel: getSortedRowModel(),
onColumnFiltersChange: setColumnFilters,
getFilteredRowModel: getFilteredRowModel(),
state: {
sorting,
columnFilters,
},
});
const itemIds = React.useMemo(
() => data.map((item) => String(getRowId ? getRowId(item) : '')),
[data, getRowId],
);
function handleDragEnd(event: DragEndEvent) {
const { active, over } = event;
if (!over || active.id === over.id) {
return;
}
const oldIndex = itemIds.indexOf(String(active.id));
const newIndex = itemIds.indexOf(String(over.id));
if (oldIndex === -1 || newIndex === -1) {
return;
}
const reordered = [...data];
const [moved] = reordered.splice(oldIndex, 1);
reordered.splice(newIndex, 0, moved);
onReorder?.(reordered);
}
return (
<>
{searchKey && (
<Input
placeholder={searchPlaceholder}
value={
(table
.getColumn(searchKey)
?.getFilterValue() as string) ?? ''
}
onChange={(event) =>
table
.getColumn(searchKey)
?.setFilterValue(event.target.value)
}
className="max-w-sm"
/>
)}
<Card className="bg-sidebar p-0">
<CardContent className="p-0">
<Table>
<TableHeader>
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => (
<TableHead
key={header.id}
className={
(
header.column.columnDef
.meta as {
headerClassName?: string;
}
)?.headerClassName
}
>
{header.isPlaceholder
? null
: flexRender(
header.column.columnDef
.header,
header.getContext(),
)}
</TableHead>
))}
</TableRow>
))}
</TableHeader>
<TableBody>
{table.getRowModel().rows?.length ? (
isSortable ? (
<DndContext
sensors={sensors}
onDragEnd={handleDragEnd}
>
<SortableContext
items={itemIds}
strategy={
verticalListSortingStrategy
}
>
{table
.getRowModel()
.rows.map((row) => (
<SortableTableRow
key={row.id}
id={String(
getRowId!(
row.original,
),
)}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={
cell.id
}
className={
(
cell
.column
.columnDef
.meta as {
className?: string;
}
)
?.className
}
>
{flexRender(
cell
.column
.columnDef
.cell,
cell.getContext(),
)}
</TableCell>
))}
</SortableTableRow>
))}
</SortableContext>
</DndContext>
) : (
table
.getRowModel()
.rows.map((row) => (
<TableRow
key={row.id}
data-state={
row.getIsSelected() &&
'selected'
}
>
{row
.getVisibleCells()
.map((cell) => (
<TableCell
key={cell.id}
className={
(
cell
.column
.columnDef
.meta as {
className?: string;
}
)?.className
}
>
{flexRender(
cell.column
.columnDef
.cell,
cell.getContext(),
)}
</TableCell>
))}
</TableRow>
))
)
) : (
<TableRow>
<TableCell
colSpan={visibleColumns.length}
className="h-24 text-center"
>
{emptyText}
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</CardContent>
</Card>
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
Halaman {table.getState().pagination.pageIndex + 1} dari{' '}
{table.getPageCount()}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => table.setPageIndex(0)}
disabled={!table.getCanPreviousPage()}
>
<ChevronsLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<ChevronLeft className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<ChevronRight className="h-4 w-4" />
</Button>
<Button
variant="outline"
size="sm"
onClick={() => table.setPageIndex(table.getPageCount() - 1)}
disabled={!table.getCanNextPage()}
>
<ChevronsRight className="h-4 w-4" />
</Button>
</div>
</div>
</>
);
}