306 lines
9.1 KiB
TypeScript
306 lines
9.1 KiB
TypeScript
import type { CheckboxProps } from '@/components/ui/Checkbox'
|
|
import Checkbox from '@/components/ui/Checkbox'
|
|
import Pagination from '@/components/ui/Pagination'
|
|
import Select from '@/components/ui/Select'
|
|
import type { SkeletonProps } from '@/components/ui/Skeleton'
|
|
import Table from '@/components/ui/Table'
|
|
import {
|
|
CellContext,
|
|
ColumnDef,
|
|
ColumnSort,
|
|
flexRender,
|
|
getCoreRowModel,
|
|
getFilteredRowModel,
|
|
getPaginationRowModel,
|
|
getSortedRowModel,
|
|
Row,
|
|
useReactTable,
|
|
} from '@tanstack/react-table'
|
|
import classNames from 'classnames'
|
|
import type { ChangeEvent, ForwardedRef } from 'react'
|
|
import { forwardRef, useEffect, useImperativeHandle, useMemo, useRef, useState } from 'react'
|
|
import TableRowSkeleton from './loaders/TableRowSkeleton'
|
|
import Loading from './Loading'
|
|
|
|
export type OnSortParam = { order: 'asc' | 'desc' | ''; key: string | number }
|
|
|
|
type DataTableProps<T> = {
|
|
columns: ColumnDef<T>[]
|
|
data?: unknown[]
|
|
loading?: boolean
|
|
onCheckBoxChange?: (checked: boolean, row: T) => void
|
|
onIndeterminateCheckBoxChange?: (checked: boolean, rows: Row<T>[]) => void
|
|
onPaginationChange?: (page: number) => void
|
|
onSelectChange?: (num: number) => void
|
|
onSort?: (sort: OnSortParam) => void
|
|
pageSizes?: number[]
|
|
selectable?: boolean
|
|
skeletonAvatarColumns?: number[]
|
|
skeletonAvatarProps?: SkeletonProps
|
|
pagingData?: {
|
|
total: number
|
|
pageIndex: number
|
|
pageSize: number
|
|
}
|
|
}
|
|
|
|
type CheckBoxChangeEvent = ChangeEvent<HTMLInputElement>
|
|
|
|
interface IndeterminateCheckboxProps extends Omit<CheckboxProps, 'onChange'> {
|
|
onChange: (event: CheckBoxChangeEvent) => void
|
|
indeterminate: boolean
|
|
onCheckBoxChange?: (event: CheckBoxChangeEvent) => void
|
|
onIndeterminateCheckBoxChange?: (event: CheckBoxChangeEvent) => void
|
|
}
|
|
|
|
const { Tr, Th, Td, THead, TBody, Sorter } = Table
|
|
|
|
const IndeterminateCheckbox = (props: IndeterminateCheckboxProps) => {
|
|
const { indeterminate, onChange, onCheckBoxChange, onIndeterminateCheckBoxChange, ...rest } =
|
|
props
|
|
|
|
const ref = useRef<HTMLInputElement>(null)
|
|
|
|
useEffect(() => {
|
|
if (typeof indeterminate === 'boolean' && ref.current) {
|
|
ref.current.indeterminate = !rest.checked && indeterminate
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [ref, indeterminate])
|
|
|
|
const handleChange = (e: CheckBoxChangeEvent) => {
|
|
onChange(e)
|
|
onCheckBoxChange?.(e)
|
|
onIndeterminateCheckBoxChange?.(e)
|
|
}
|
|
|
|
return <Checkbox ref={ref} className="mb-0" onChange={(_, e) => handleChange(e)} {...rest} />
|
|
}
|
|
|
|
export type DataTableResetHandle = {
|
|
resetSorting: () => void
|
|
resetSelected: () => void
|
|
}
|
|
|
|
function DataTableBase<T>(props: DataTableProps<T>, ref: ForwardedRef<DataTableResetHandle>) {
|
|
const {
|
|
skeletonAvatarColumns,
|
|
columns: columnsProp = [],
|
|
data = [],
|
|
loading = false,
|
|
onCheckBoxChange,
|
|
onIndeterminateCheckBoxChange,
|
|
onPaginationChange,
|
|
onSelectChange,
|
|
onSort,
|
|
pageSizes = [10, 25, 50, 100],
|
|
selectable = false,
|
|
skeletonAvatarProps,
|
|
pagingData = {
|
|
total: 0,
|
|
pageIndex: 1,
|
|
pageSize: 10,
|
|
},
|
|
} = props
|
|
|
|
const { pageSize, pageIndex, total } = pagingData
|
|
|
|
const [sorting, setSorting] = useState<ColumnSort[] | null>(null)
|
|
|
|
const pageSizeOption = useMemo(
|
|
() =>
|
|
pageSizes.map((number) => ({
|
|
value: number,
|
|
label: `${number} / page`,
|
|
})),
|
|
[pageSizes],
|
|
)
|
|
|
|
const handleCheckBoxChange = (checked: boolean, row: T) => {
|
|
if (!loading) {
|
|
onCheckBoxChange?.(checked, row)
|
|
}
|
|
}
|
|
|
|
const handleIndeterminateCheckBoxChange = (checked: boolean, rows: Row<T>[]) => {
|
|
if (!loading) {
|
|
onIndeterminateCheckBoxChange?.(checked, rows)
|
|
}
|
|
}
|
|
|
|
const handlePaginationChange = (page: number) => {
|
|
if (!loading) {
|
|
onPaginationChange?.(page)
|
|
}
|
|
}
|
|
|
|
const handleSelectChange = (value?: number) => {
|
|
if (!loading) {
|
|
onSelectChange?.(Number(value))
|
|
}
|
|
}
|
|
|
|
useEffect(() => {
|
|
if (Array.isArray(sorting)) {
|
|
const sortOrder = sorting.length > 0 ? (sorting[0].desc ? 'desc' : 'asc') : ''
|
|
const id = sorting.length > 0 ? sorting[0].id : ''
|
|
onSort?.({ order: sortOrder, key: id })
|
|
}
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [sorting])
|
|
|
|
const finalColumns: ColumnDef<T>[] = useMemo(() => {
|
|
const columns = columnsProp
|
|
|
|
if (selectable) {
|
|
return [
|
|
{
|
|
id: 'select',
|
|
header: ({ table }) => (
|
|
<IndeterminateCheckbox
|
|
checked={table.getIsAllRowsSelected()}
|
|
indeterminate={table.getIsSomeRowsSelected()}
|
|
onChange={table.getToggleAllRowsSelectedHandler()}
|
|
onIndeterminateCheckBoxChange={(e) => {
|
|
handleIndeterminateCheckBoxChange(e.target.checked, table.getRowModel().rows)
|
|
}}
|
|
/>
|
|
),
|
|
cell: ({ row }) => (
|
|
<IndeterminateCheckbox
|
|
checked={row.getIsSelected()}
|
|
disabled={!row.getCanSelect()}
|
|
indeterminate={row.getIsSomeSelected()}
|
|
onChange={row.getToggleSelectedHandler()}
|
|
onCheckBoxChange={(e) => handleCheckBoxChange(e.target.checked, row.original)}
|
|
/>
|
|
),
|
|
},
|
|
...columns,
|
|
]
|
|
}
|
|
return columns
|
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
}, [columnsProp, selectable])
|
|
|
|
const table = useReactTable({
|
|
data,
|
|
columns: finalColumns as ColumnDef<unknown | object | any[], any>[],
|
|
getCoreRowModel: getCoreRowModel(),
|
|
getFilteredRowModel: getFilteredRowModel(),
|
|
getPaginationRowModel: getPaginationRowModel(),
|
|
getSortedRowModel: getSortedRowModel(),
|
|
manualPagination: true,
|
|
manualSorting: true,
|
|
onSortingChange: (sorter) => {
|
|
setSorting(sorter as ColumnSort[])
|
|
},
|
|
// Satir secimi tablonun kendi durumunda tutulur. Disaridan kontrol edilen bir
|
|
// `rowSelection` degeri `onRowSelectionChange` olmadan degisimi yutuyor, onay kutusu
|
|
// hicbir zaman isaretlenmiyordu; secilen satirlari cagiran taraf `onCheckBoxChange`
|
|
// ile zaten kendi durumunda biriktiriyor.
|
|
state: {
|
|
sorting: sorting as ColumnSort[],
|
|
},
|
|
})
|
|
|
|
const resetSorting = () => {
|
|
table.resetSorting()
|
|
}
|
|
|
|
const resetSelected = () => {
|
|
table.toggleAllRowsSelected(false)
|
|
}
|
|
|
|
useImperativeHandle(ref, () => ({
|
|
resetSorting,
|
|
resetSelected,
|
|
}))
|
|
|
|
return (
|
|
<Loading loading={loading && data.length !== 0} type="cover">
|
|
<Table>
|
|
<THead>
|
|
{table.getHeaderGroups().map((headerGroup) => (
|
|
<Tr key={headerGroup.id}>
|
|
{headerGroup.headers.map((header) => {
|
|
return (
|
|
<Th key={header.id} colSpan={header.colSpan}>
|
|
{header.isPlaceholder ? null : (
|
|
<div
|
|
className={classNames(
|
|
header.column.getCanSort() && 'cursor-pointer select-none point',
|
|
loading && 'pointer-events-none',
|
|
)}
|
|
onClick={header.column.getToggleSortingHandler()}
|
|
>
|
|
{flexRender(header.column.columnDef.header, header.getContext())}
|
|
{header.column.getCanSort() && (
|
|
<Sorter sort={header.column.getIsSorted()} />
|
|
)}
|
|
</div>
|
|
)}
|
|
</Th>
|
|
)
|
|
})}
|
|
</Tr>
|
|
))}
|
|
</THead>
|
|
{loading && data.length === 0 ? (
|
|
<TableRowSkeleton
|
|
columns={(finalColumns as Array<T>).length}
|
|
rows={pagingData.pageSize}
|
|
avatarInColumns={skeletonAvatarColumns}
|
|
avatarProps={skeletonAvatarProps}
|
|
/>
|
|
) : (
|
|
<TBody>
|
|
{table
|
|
.getRowModel()
|
|
.rows.slice(0, pageSize)
|
|
.map((row) => {
|
|
return (
|
|
<Tr key={row.id}>
|
|
{row.getVisibleCells().map((cell) => {
|
|
return (
|
|
<Td key={cell.id}>
|
|
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
|
</Td>
|
|
)
|
|
})}
|
|
</Tr>
|
|
)
|
|
})}
|
|
</TBody>
|
|
)}
|
|
</Table>
|
|
<div className="flex items-center justify-between mt-4">
|
|
<Pagination
|
|
pageSize={pageSize}
|
|
currentPage={pageIndex}
|
|
total={total}
|
|
onChange={handlePaginationChange}
|
|
/>
|
|
<div style={{ minWidth: 130 }}>
|
|
<Select
|
|
size="sm"
|
|
menuPlacement="top"
|
|
isSearchable={false}
|
|
value={pageSizeOption.filter((option) => option.value === pageSize)}
|
|
options={pageSizeOption}
|
|
onChange={(option) => handleSelectChange(option?.value)}
|
|
/>
|
|
</div>
|
|
</div>
|
|
</Loading>
|
|
)
|
|
}
|
|
|
|
const DataTable = forwardRef(DataTableBase) as <T>(
|
|
props: DataTableProps<T> & {
|
|
ref?: ForwardedRef<DataTableResetHandle>
|
|
},
|
|
) => ReturnType<typeof DataTableBase>
|
|
|
|
export type { CellContext, ColumnDef, Row }
|
|
export default DataTable
|