reponsive and selective actions and row actioin + pagination

This commit is contained in:
hamid zarghami
2025-06-17 20:52:40 +03:30
parent 71fd53c810
commit 4c869c42c6
24 changed files with 869 additions and 287 deletions
+27 -27
View File
@@ -1,40 +1,40 @@
import { ButtonHTMLAttributes, FC, memo, ReactNode } from 'react'
import MoonLoader from "react-spinners/MoonLoader"
import { XOR } from '../helpers/types'
import { FC, ReactNode } from 'react'
import { clx } from '../helpers/utils'
type Props = {
interface ButtonProps {
label?: string;
children?: ReactNode;
className?: string;
isLoading?: boolean,
percentage?: number,
} & ButtonHTMLAttributes<HTMLButtonElement> &
XOR<{ children: ReactNode }, { label: string }>;
loading?: boolean;
onClick?: () => void;
type?: 'button' | 'submit' | 'reset';
disabled?: boolean;
}
const Button: FC<Props> = memo((props: Props) => {
const Button: FC<ButtonProps> = (props) => {
const buttonClass = clx(
'flex rounded-xl items-center justify-center text-center h-10 text-sm bg-primary text-white w-full',
props.disabled && 'cursor-not-allowed opacity-60',
'w-full bg-primary text-white rounded-xl font-normal text-xs md:text-sm h-10 px-3 md:px-4 transition-all duration-200 hover:bg-opacity-90 disabled:opacity-50 disabled:cursor-not-allowed',
props.className
);
)
return (
<button {...props} className={`${buttonClass} ${props.className}`} >
{
props.isLoading ?
<div className='flex gap-2 items-center'>
{
!props.percentage ?
<MoonLoader size={20} color='#fff' />
:
<span>{props.percentage}%</span>
}
</div>
:
props.label || props.children
}
<button
onClick={props.onClick}
type={props.type || 'button'}
disabled={props.disabled || props.loading}
className={buttonClass}
>
{props.loading ? (
<div className="flex items-center justify-center gap-2">
<div className="w-3 h-3 md:w-4 md:h-4 border-2 border-white border-t-transparent rounded-full animate-spin"></div>
درحال بارگذاری...
</div>
) : (
props.children || props.label
)}
</button>
)
})
}
export default Button
+1 -1
View File
@@ -12,7 +12,7 @@ const DefaultTableSkeleton: FC<Props> = ({ tdCount, trCount = 5 }) => {
<Fragment>
{
Array.from({ length: trCount }).map((_, rowIndex) => (
<tr className="tr" key={rowIndex}>
<tr className="w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5]" key={rowIndex}>
{
Array.from({ length: tdCount }).map((_, colIndex) => (
<Td text={''} key={colIndex}>
+4 -4
View File
@@ -44,8 +44,8 @@ const Filters: FC<FiltersProps> = ({
fields,
onChange,
initialValues = {},
className = "mt-10 flex justify-between items-center",
fieldClassName = "flex gap-4",
className = "mt-6 md:mt-8 xl:mt-10 flex flex-col md:flex-row justify-between items-start md:items-center gap-4",
fieldClassName = "flex flex-col sm:flex-row gap-3 md:gap-4 w-full md:w-auto",
searchField = "search" // پیش‌فرض فیلد سرچ با نام search
}) => {
const [filters, setFilters] = useState<FilterValues>({});
@@ -111,7 +111,7 @@ const Filters: FC<FiltersProps> = ({
key={field.name}
placeholder={field.placeholder}
variant="search"
className="max-w-[200px]"
className="w-full md:max-w-[200px]"
value={currentValue || field.defaultValue || ''}
onChange={(e) => handleInputChange(field.name, e)}
/>
@@ -129,7 +129,7 @@ const Filters: FC<FiltersProps> = ({
{otherFields.map(renderField)}
</div>
{searchFieldObj && (
<div>
<div className="w-full md:w-auto">
{renderField(searchFieldObj)}
</div>
)}
-1
View File
@@ -96,7 +96,6 @@ const Input: FC<Props> = (props: Props) => {
{
props.type === 'password' &&
<Eye onClick={() => setShowPassword((oldValue) => !oldValue)} size={20} color='#8C90A3' className='absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
// <img onClick={() => setShowPassword((oldValue) => !oldValue)} src={EyeIcon} className='w-5 absolute top-0 bottom-0 cursor-pointer my-auto left-3' />
}
{
+109
View File
@@ -0,0 +1,109 @@
import React from "react";
interface PaginationProps {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
const Pagination: React.FC<PaginationProps> = ({
currentPage,
totalPages,
onPageChange,
}) => {
// اگر فقط یک صفحه داریم، pagination نمایش داده نمی‌شود
if (totalPages <= 1) return null;
const getPageNumbers = () => {
const pageNumbers: (number | string)[] = [];
const maxVisiblePages = window.innerWidth < 768 ? 3 : 5; // تعداد کمتر برای موبایل
if (totalPages <= maxVisiblePages) {
for (let i = 1; i <= totalPages; i++) {
pageNumbers.push(i);
}
} else {
// نمایش صفحه اول
pageNumbers.push(1);
if (currentPage > 3) {
pageNumbers.push("...");
}
// صفحات میانی
const start = Math.max(2, Math.min(currentPage - 1, totalPages - 3));
const end = Math.min(totalPages - 1, Math.max(currentPage + 1, 4));
for (let i = start; i <= end; i++) {
if (!pageNumbers.includes(i)) {
pageNumbers.push(i);
}
}
if (currentPage < totalPages - 2) {
pageNumbers.push("...");
}
// نمایش صفحه آخر
if (!pageNumbers.includes(totalPages)) {
pageNumbers.push(totalPages);
}
}
return pageNumbers;
};
const pageNumbers = getPageNumbers();
return (
<div className="flex justify-center items-center gap-1 md:gap-2 py-4 md:py-6 mt-3 md:mt-4 border-t border-gray-100">
{/* دکمه صفحه قبل */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === 1
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage > 1 && onPageChange(currentPage - 1)}
disabled={currentPage === 1}
>
قبلی
</button>
{/* شماره صفحات */}
<div className="flex gap-1">
{pageNumbers.map((page, index) =>
typeof page === "number" ? (
<button
key={index}
className={`w-8 h-8 md:w-10 md:h-10 text-xs md:text-sm rounded-lg transition-colors ${currentPage === page
? "bg-primary text-white shadow-sm"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => onPageChange(page)}
>
{page}
</button>
) : (
<span key={index} className="flex items-center px-1 md:px-2 text-gray-400 text-xs md:text-sm">
{page}
</span>
)
)}
</div>
{/* دکمه صفحه بعد */}
<button
className={`px-2 md:px-3 py-2 text-xs md:text-sm rounded-lg transition-colors ${currentPage === totalPages
? "text-gray-400 cursor-not-allowed"
: "text-gray-600 hover:bg-gray-100 hover:text-gray-800"
}`}
onClick={() => currentPage < totalPages && onPageChange(currentPage + 1)}
disabled={currentPage === totalPages}
>
بعدی
</button>
</div>
);
};
export default Pagination;
+117
View File
@@ -0,0 +1,117 @@
import React, { useState, useRef, useEffect } from 'react';
import { createPortal } from 'react-dom';
import { More } from 'iconsax-react';
export interface RowActionItem {
label: string;
icon?: React.ReactNode;
onClick: () => void;
className?: string;
}
interface RowActionsDropdownProps {
actions: RowActionItem[];
className?: string;
}
const RowActionsDropdown: React.FC<RowActionsDropdownProps> = ({ actions, className = '' }) => {
const [isOpen, setIsOpen] = useState(false);
const [position, setPosition] = useState({ top: 0, left: 0 });
const dropdownRef = useRef<HTMLDivElement>(null);
const buttonRef = useRef<HTMLButtonElement>(null);
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) {
setIsOpen(false);
}
};
document.addEventListener('mousedown', handleClickOutside);
return () => {
document.removeEventListener('mousedown', handleClickOutside);
};
}, []);
const handleToggle = (e: React.MouseEvent) => {
e.stopPropagation();
if (!isOpen && buttonRef.current) {
const rect = buttonRef.current.getBoundingClientRect();
const isMobile = window.innerWidth < 768;
const dropdownWidth = isMobile ? 140 : 150;
const dropdownHeight = actions.length * (isMobile ? 36 : 40); // تخمین ارتفاع منو
let left = rect.left + window.scrollX - dropdownWidth + rect.width;
let top = rect.bottom + window.scrollY;
// بررسی اینکه منو از سمت راست خارج نشود
if (left + dropdownWidth > window.innerWidth) {
left = rect.left + window.scrollX - dropdownWidth;
}
// بررسی اینکه منو از سمت چپ خارج نشود
if (left < 0) {
left = isMobile ? 20 : 40;
}
// بررسی اینکه منو از پایین خارج نشود
if (top + dropdownHeight > window.innerHeight + window.scrollY) {
top = rect.top + window.scrollY - dropdownHeight;
}
setPosition({ top, left });
}
setIsOpen(!isOpen);
};
const handleActionClick = (action: RowActionItem) => {
action.onClick();
setIsOpen(false);
};
const renderDropdown = () => {
if (!isOpen) return null;
return createPortal(
<div
ref={dropdownRef}
className="fixed py-1 md:py-2 bg-white rounded-xl md:rounded-2xl shadow-lg z-[9999] min-w-[140px] md:min-w-[150px]"
style={{
top: `${position.top}px`,
left: `${position.left}px`,
}}
>
{actions.map((action, index) => (
<button
key={index}
onClick={() => handleActionClick(action)}
className={`w-full text-right px-2 md:px-3 py-1 md:py-1.5 text-xs md:text-[13px] flex items-center gap-2 hover:bg-gray-50 ${index === 0 ? 'rounded-t-lg' : ''
} ${action.className || ''}`}
>
{action.icon && <span className="ml-2">{action.icon}</span>}
{action.label}
</button>
))}
</div>,
document.body
);
};
return (
<div className={className}>
<button
ref={buttonRef}
onClick={handleToggle}
className="p-1 hover:bg-gray-100 rounded-full transition-colors"
>
<More size={14} color="black" className="rotate-90" />
</button>
{renderDropdown()}
</div>
);
};
export default RowActionsDropdown;
+119 -50
View File
@@ -3,6 +3,8 @@ import DefaultTableSkeleton from './DefaultTableSkeleton';
import Td from './Td';
import { TableProps, RowDataType, ColumnType } from './types/TableTypes';
import { Checkbox } from './ui/checkbox';
import RowActionsDropdown from './RowActionsDropdown';
import Pagination from './Pagination';
const Table = <T extends RowDataType>({
columns,
@@ -18,6 +20,10 @@ const Table = <T extends RowDataType>({
actions = null,
actionsPosition = 'top',
selectable = false,
selectedActions = null,
onSelectionChange,
rowActions,
pagination,
}: TableProps<T>): React.ReactElement => {
const [selectedRows, setSelectedRows] = useState<T[]>([]);
@@ -31,36 +37,42 @@ const Table = <T extends RowDataType>({
const getCellClassName = (column: ColumnType<T>): string => {
const alignClass = column.align ? `text-${column.align}` : '';
return `p-3 ${alignClass} ${column.className || ''}`.trim();
return `px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs ${alignClass} ${column.className || ''}`.trim();
};
const handleRowClick = (item: T) => {
if (selectable) {
if (selectedRows.includes(item)) {
setSelectedRows(selectedRows.filter((row) => row !== item));
} else {
setSelectedRows([...selectedRows, item]);
}
}
if (onRowClick) {
onRowClick(item.id, item);
}
};
const handleSelectAll = () => {
if (selectedRows.length === data.length) {
setSelectedRows([]);
} else {
setSelectedRows([...data]);
const newSelectedRows = selectedRows.length === data.length ? [] : [...data];
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
};
const handleSingleRowSelect = (item: T, checked: boolean) => {
const newSelectedRows = checked
? [...selectedRows, item]
: selectedRows.filter((row) => row.id !== item.id);
setSelectedRows(newSelectedRows);
if (onSelectionChange) {
onSelectionChange(newSelectedRows);
}
};
const isRowSelected = (item: T): boolean => {
return selectedRows.some((row) => row.id === item.id);
};
const renderTableBody = () => {
if (isLoading) {
return (
<Fragment>
<DefaultTableSkeleton tdCount={columns.length} trCount={emptyRowsCount} />
<DefaultTableSkeleton tdCount={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} trCount={emptyRowsCount} />
</Fragment>
);
}
@@ -68,7 +80,7 @@ const Table = <T extends RowDataType>({
if (data.length === 0) {
return (
<tr>
<td colSpan={columns.length} className="p-8 text-center text-gray-500">
<td colSpan={columns.length + (selectable ? 1 : 0) + (rowActions ? 1 : 0)} className="px-3 md:px-6 py-6 md:py-8 text-center text-gray-500">
{noDataMessage}
</td>
</tr>
@@ -77,28 +89,31 @@ const Table = <T extends RowDataType>({
return data.map((item, rowIndex) => (
<tr
key={item.id}
className={`tr border-b hover:bg-gray-50 ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
className={`w-full h-[64px] md:h-[74px] bg-white border-t border-[#EAEDF5] hover:bg-[#F4F5F9] ${onRowClick ? 'cursor-pointer' : ''} ${getRowClassName(item, rowIndex)}`}
onClick={() => handleRowClick(item)}
>
{selectable && (
<td className="p-3 w-10 relative z-[5]">
<td className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<Checkbox
checked={selectedRows.includes(item)}
onCheckedChange={(checked) => {
if (checked) {
setSelectedRows([...selectedRows, item]);
} else {
setSelectedRows(selectedRows.filter((row) => row !== item));
}
}}
checked={isRowSelected(item)}
onCheckedChange={(checked) => handleSingleRowSelect(item, !!checked)}
/>
</td>
)}
{columns.map((col) => (
<td key={`${item.id}-${col.key}`} className={getCellClassName(col)} style={{ width: col.width }}>
<td
key={`${item.id}-${col.key}`}
className={getCellClassName(col)}
style={{ width: col.width }}
>
{col.render ? col.render(item) : item[col.key] as React.ReactNode}
</td>
))}
{rowActions && (
<td className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<RowActionsDropdown actions={rowActions(item)} />
</td>
)}
</tr>
));
};
@@ -107,12 +122,10 @@ const Table = <T extends RowDataType>({
if (!showHeader || actionsPosition === 'header-replace') return null;
return (
<thead className={`thead ${headerClassName}`}>
<thead className={`h-[60px] md:h-[69px] bg-white text-sm text-[#8C90A3] ${headerClassName}`}>
<tr>
{selectable && (
<th
className="p-3 w-10 relative z-[5]"
>
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10 relative z-[5]">
<Checkbox
checked={data.length > 0 && selectedRows.length === data.length}
onCheckedChange={handleSelectAll}
@@ -128,41 +141,97 @@ const Table = <T extends RowDataType>({
<Td text={col.title} />
</th>
))}
{rowActions && (
<th className="px-3 md:px-6 py-3 md:py-4 w-8 md:w-10"></th>
)}
</tr>
</thead>
);
};
const renderActions = () => {
if (!actions) return null;
if (!actions && (!selectable || selectedRows.length === 0 || !selectedActions)) return null;
return (
<div className={'action'}>
{actions}
<div className="h-[64px] rounded-t-2xl md:rounded-t-3xl md:h-[74px] bg-white flex items-center px-3 md:px-6">
<div className='flex items-center gap-2 md:gap-4'>
{actions}
{selectable && selectedRows.length > 0 && selectedActions && selectedActions}
</div>
</div>
);
};
return (
<div className={`relative overflow-x-auto rounded-3xl mt-9 w-full ${className}`}>
<div className={`relative mt-6 md:mt-9 w-full ${className}`}>
{(actionsPosition === 'top' || actionsPosition === 'above-header') && renderActions()}
<table className="w-full text-sm border-collapse">
{renderHeader()}
{actionsPosition === 'header-replace' && (
<thead>
<tr>
<th colSpan={columns.length} className="p-0">
{renderActions()}
</th>
</tr>
</thead>
)}
<tbody>
{renderTableBody()}
</tbody>
</table>
<div className="overflow-x-auto overflow-hidden rounded-b-2xl md:rounded-b-3xl bg-white shadow-sm">
<table className="w-full text-sm border-collapse min-w-[600px]">
{renderHeader()}
{actionsPosition === 'header-replace' && (
<thead>
<tr>
<th colSpan={columns.length} className="p-0">
{renderActions()}
</th>
</tr>
</thead>
)}
<tbody>
{renderTableBody()}
</tbody>
</table>
</div>
{pagination && (
<Pagination
currentPage={pagination.currentPage}
totalPages={pagination.totalPages}
onPageChange={pagination.onPageChange}
/>
)}
</div>
);
};
export default Table;
export default Table;
/*
مثال استفاده با pagination:
import Table from '@/components/Table';
import { useState } from 'react';
const MyComponent = () => {
const [currentPage, setCurrentPage] = useState(1);
const totalPages = 10;
const columns = [
{ title: 'نام', key: 'name' },
{ title: 'ایمیل', key: 'email' },
];
const data = [
{ id: 1, name: 'علی', email: 'ali@example.com' },
{ id: 2, name: 'فاطمه', email: 'fateme@example.com' },
];
const handlePageChange = (page: number) => {
setCurrentPage(page);
// اینجا می‌توانید API call جدید برای دریافت داده‌های صفحه جدید بزنید
};
return (
<Table
columns={columns}
data={data}
pagination={{
currentPage,
totalPages,
onPageChange: handlePageChange
}}
/>
);
};
*/
+5 -5
View File
@@ -24,19 +24,19 @@ const Tabs: FC<Props> = (props: Props) => {
return (
<Swiper
slidesPerView='auto'
spaceBetween={30}
className='px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-10 flex h-[80px] rounded-[32px] bg-white bg-opacity-45'>
spaceBetween={20}
className='px-4 md:px-8 xl:px-10 max-w-full w-fit items-center text-description mx-auto backdrop-blur-md border-2 border-white gap-4 md:gap-6 xl:gap-10 flex h-[60px] md:h-[70px] xl:h-[80px] rounded-[20px] md:rounded-[28px] xl:rounded-[32px] bg-white bg-opacity-45'>
{
props.items.map((item: Item, index: number) => {
return (
<SwiperSlide style={SWIPER_SLIDE_STYLE} onClick={() => props.onChange(item.value)} key={item.value} className={clx(
'flex flex-col max-w-fit mt-[15px] items-center gap-2 cursor-pointer',
index === 0 && 'pr-[30px]',
'flex flex-col max-w-fit mt-[10px] md:mt-[12px] xl:mt-[15px] items-center gap-1 md:gap-2 cursor-pointer',
index === 0 && 'pr-[15px] md:pr-[20px] xl:pr-[30px]',
index === props.items.length - 1 && props.items.length > 3 && 'pl-[5px]',
props.active === item.value && 'text-black'
)}>
{item.icon}
<div className='text-xs'>
<div className='text-[10px] md:text-xs'>
{item.label}
</div>
</SwiperSlide>
+1 -1
View File
@@ -8,7 +8,7 @@ interface Props {
const Td: FC<Props> = (props: Props) => {
return (
<td className='td' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
<td className='px-3 md:px-6 py-3 md:py-4 whitespace-nowrap text-xs' style={{ direction: props.dir === "ltr" ? "ltr" : "rtl" }}>
{
props.text ?
props.text
+31 -30
View File
@@ -17,59 +17,60 @@ const NewMessage: FC = () => {
{openNewMessage && (
<>
<div
className='fixed inset-0 bg-opacity-50 z-[9998]'
className='fixed inset-0 bg-black/50 z-[9998]'
onClick={() => setOpenNewMessage(false)}
// style={{ left: '250px' }}
/>
<div className={`fixed left-8 bottom-4 bg-white rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] transition-all duration-300 p-8 w-[800px] `}>
<div className='fixed left-2 right-2 bottom-2 md:left-4 md:right-4 md:bottom-4 md:top-4 xl:left-8 xl:right-auto xl:bottom-4 xl:top-4 bg-white rounded-2xl md:rounded-3xl shadow-[0_0_20px_rgba(0,0,0,0.1)] z-[9999] transition-all duration-300 p-4 md:p-6 xl:p-8 w-auto xl:w-[800px] max-h-[90vh] overflow-y-auto'>
{/* Header */}
<div className='flex justify-between items-center'>
<div className=''>
<div className='flex justify-between items-center mb-6 md:mb-8'>
<div className='text-lg md:text-xl xl:text-2xl font-semibold'>
{t('new_message.title')}
</div>
<div onClick={() => setOpenNewMessage(false)}>
<CloseCircle size={24} color='#292D32' />
<div onClick={() => setOpenNewMessage(false)} className='cursor-pointer p-1'>
<CloseCircle size={20} className='md:w-6 md:h-6' color='#292D32' />
</div>
</div>
{/* Content */}
<div className='mt-10'>
<div className='space-y-6 md:space-y-8'>
<Input
label={t('new_message.to')}
/>
<div className='mt-8'>
<Input
label={t('new_message.subject')}
<Input
label={t('new_message.subject')}
/>
<div>
<ReactQuill
modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}}
theme="snow"
value={value}
onChange={setValue}
style={{ minHeight: '120px' }}
className='text-sm'
/>
</div>
<div className='mt-8'>
<ReactQuill modules={{
toolbar: [
[{ header: '1' }, { header: '2' }, { font: [] }],
[{ list: 'ordered' }, { list: 'bullet' }],
['bold', 'italic', 'underline'],
['link', 'image'],
[{ align: [] }],
['clean']
]
}} theme="snow" value={value} onChange={setValue} />
</div>
<div className='mt-8 flex justify-end gap-4'>
<div className='flex flex-col sm:flex-row justify-end gap-3 md:gap-4 pt-4'>
<Button
className='!w-fit px-10 bg-white text-black border border-primary'
className='!w-full sm:!w-fit px-6 md:px-10 bg-white text-black border border-primary order-2 sm:order-1'
label={t('new_message.draft')}
/>
<Button
className='w-fit px-10'
className='w-full sm:w-fit px-6 md:px-10 order-1 sm:order-2'
label={t('new_message.send')}
/>
</div>
</div>
</div>
</>
+11
View File
@@ -1,4 +1,5 @@
import { ReactNode } from "react";
import { RowActionItem } from "../RowActionsDropdown";
export type RowDataType = Record<string, unknown> & { id: string | number };
@@ -12,6 +13,12 @@ export interface ColumnType<T extends RowDataType = RowDataType> {
className?: string;
}
export interface PaginationConfig {
currentPage: number;
totalPages: number;
onPageChange: (page: number) => void;
}
export interface TableProps<T extends RowDataType = RowDataType> {
columns: ColumnType<T>[];
data: T[];
@@ -27,4 +34,8 @@ export interface TableProps<T extends RowDataType = RowDataType> {
actionsClassName?: string;
actionsPosition?: "top" | "header-replace" | "above-header";
selectable?: boolean;
selectedActions?: ReactNode;
onSelectionChange?: (selectedRows: T[]) => void;
rowActions?: (item: T) => RowActionItem[];
pagination?: PaginationConfig;
}