Files
MikrotikManager/components/reui/data-grid/data-grid-pagination.tsx
T

237 lines
7.5 KiB
TypeScript

"use client"
import type { JSX, ReactNode } from "react"
import { useDataGrid } from "@/components/reui/data-grid/data-grid"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { Skeleton } from "@/components/ui/skeleton"
import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react"
interface DataGridPaginationProps {
sizes?: number[]
sizesInfo?: string
sizesLabel?: string
sizesDescription?: string
sizesSkeleton?: ReactNode
more?: boolean
moreLimit?: number
info?: string
infoSkeleton?: ReactNode
className?: string
rowsPerPageLabel?: string
previousPageLabel?: string
nextPageLabel?: string
ellipsisText?: string
}
function DataGridPagination(props: DataGridPaginationProps): JSX.Element {
const { i18n, table, recordCount, isLoading } = useDataGrid()
const defaultProps: Partial<DataGridPaginationProps> = {
sizes: [5, 10, 25, 50, 100],
sizesSkeleton: <Skeleton className="h-8 w-44" />,
moreLimit: 5,
infoSkeleton: <Skeleton className="h-8 w-60" />,
rowsPerPageLabel: i18n.labels.rowsPerPage,
previousPageLabel: i18n.labels.previousPage,
nextPageLabel: i18n.labels.nextPage,
ellipsisText: i18n.labels.paginationEllipsis,
}
const mergedProps: DataGridPaginationProps = { ...defaultProps, ...props }
const btnBaseClasses = "p-0 text-sm"
const btnArrowClasses = btnBaseClasses + " rtl:transform rtl:rotate-180"
const pageIndex = table.state.pagination.pageIndex
const pageSize = table.state.pagination.pageSize
const from = recordCount === 0 ? 0 : pageIndex * pageSize + 1
const to = Math.min((pageIndex + 1) * pageSize, recordCount)
const pageCount = table.getPageCount()
// A supplied `info` keeps its placeholder-template contract; the default
// routes through the i18n label function, where word order is free.
const paginationInfo = mergedProps.info
? mergedProps.info
.replaceAll("{from}", from.toString())
.replaceAll("{to}", to.toString())
.replaceAll("{count}", recordCount.toString())
: i18n.labels.paginationInfo({ from, to, count: recordCount })
// Pagination limit logic
const paginationMoreLimit = mergedProps.moreLimit || 5
// Determine the start and end of the pagination group
const currentGroupStart =
Math.floor(pageIndex / paginationMoreLimit) * paginationMoreLimit
const currentGroupEnd = Math.min(
currentGroupStart + paginationMoreLimit,
pageCount
)
// Render page buttons based on the current group
const renderPageButtons = () => {
const buttons = []
for (let i = currentGroupStart; i < currentGroupEnd; i++) {
buttons.push(
<Button
key={i}
size="icon-sm"
variant="ghost"
aria-label={i18n.labels.goToPage(i + 1)}
aria-current={pageIndex === i ? "page" : undefined}
className={cn(btnBaseClasses, "text-muted-foreground", {
"bg-accent text-accent-foreground": pageIndex === i,
})}
onClick={() => {
if (pageIndex !== i) {
table.setPageIndex(i)
}
}}
>
{i + 1}
</Button>
)
}
return buttons
}
// Render a "previous" ellipsis button if there are previous pages to show
const renderEllipsisPrevButton = () => {
if (currentGroupStart > 0) {
return (
<Button
size="icon-sm"
className={btnBaseClasses}
variant="ghost"
onClick={() => table.setPageIndex(currentGroupStart - 1)}
>
{mergedProps.ellipsisText}
</Button>
)
}
return null
}
// Render a "next" ellipsis button if there are more pages to show after the current group
const renderEllipsisNextButton = () => {
if (currentGroupEnd < pageCount) {
return (
<Button
className={btnBaseClasses}
variant="ghost"
size="icon-sm"
onClick={() => table.setPageIndex(currentGroupEnd)}
>
{mergedProps.ellipsisText}
</Button>
)
}
return null
}
return (
<div
data-slot="data-grid-pagination"
className={cn(
"flex grow flex-col flex-wrap items-center justify-between gap-2.5 py-2.5 sm:flex-row sm:py-0",
mergedProps.className
)}
>
<div className="order-2 flex flex-wrap items-center space-x-2.5 pb-2.5 sm:order-1 sm:pb-0">
{isLoading ? (
mergedProps.sizesSkeleton
) : (
<>
<div className="text-muted-foreground text-sm">
{mergedProps.rowsPerPageLabel}
</div>
<Select
value={`${pageSize}`}
onValueChange={(value) => {
const newPageSize = Number(value)
table.setPageSize(newPageSize)
}}
>
{/* w-fit with a min, never a fixed width: a fixed w-16 clipped
the value "100" by 1px at nova's paddings, while fit-content
grows the trigger for 3-digit sizes and the min keeps the
1-2 digit ones from collapsing narrower than 64px. */}
<SelectTrigger
aria-label={mergedProps.rowsPerPageLabel}
className="w-fit min-w-16"
size="sm"
>
<SelectValue />
</SelectTrigger>
<SelectContent
align="start"
alignItemWithTrigger={false}
className="min-w-(--anchor-width)"
>
{mergedProps.sizes?.map((size: number) => (
<SelectItem key={size} value={`${size}`}>
{size}
</SelectItem>
))}
</SelectContent>
</Select>
</>
)}
</div>
<div className="order-1 flex flex-col items-center justify-center gap-2.5 pt-2.5 sm:order-2 sm:flex-row sm:justify-end sm:pt-0">
{isLoading ? (
mergedProps.infoSkeleton
) : (
<>
<div className="text-muted-foreground order-2 text-sm text-nowrap sm:order-1">
{paginationInfo}
</div>
{pageCount > 1 && (
<div className="order-1 flex items-center space-x-1">
<Button
size="icon-sm"
variant="ghost"
className={btnArrowClasses}
onClick={() => table.previousPage()}
disabled={!table.getCanPreviousPage()}
>
<span className="sr-only">
{mergedProps.previousPageLabel}
</span>
<ChevronLeftIcon className="size-4" />
</Button>
{renderEllipsisPrevButton()}
{renderPageButtons()}
{renderEllipsisNextButton()}
<Button
size="icon-sm"
variant="ghost"
className={btnArrowClasses}
onClick={() => table.nextPage()}
disabled={!table.getCanNextPage()}
>
<span className="sr-only">{mergedProps.nextPageLabel}</span>
<ChevronRightIcon className="size-4" />
</Button>
</div>
)}
</>
)}
</div>
</div>
)
}
export { DataGridPagination, type DataGridPaginationProps }