first
This commit is contained in:
@@ -0,0 +1,198 @@
|
||||
import type { SortKey } from 'hooks/useDataControl';
|
||||
import { useState, useEffect } from 'react';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { ArrowUpDown } from 'lucide-react';
|
||||
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
||||
import { useDataControl, type UseDataControlReturn } from 'hooks/useDataControl';
|
||||
import { PaginationBar } from './PaginationBar';
|
||||
import { cn } from 'helpers/cn';
|
||||
|
||||
export const DataTable = <T,>(props: DataTableProps<T>) => {
|
||||
const [columns, setColumns] = useState<Column<T>[]>([]);
|
||||
const { dataController, uniqueKey, noDataCTA, noDataMessage } = props;
|
||||
const theUniqueKey = uniqueKey || 'id';
|
||||
const { rawData, data, pageSize, sortBy, sortedBy, setCustomSort } = dataController;
|
||||
|
||||
useEffect(() => {
|
||||
if (props.columns) {
|
||||
setColumns(props.columns);
|
||||
} else if (!!rawData && rawData.length > 0) {
|
||||
const cols: Column<T>[] = Object.keys(rawData[0] as object).map((key) => ({
|
||||
field: key as keyof T,
|
||||
label: toLabel(key),
|
||||
sortKey: key as SortKey<T>,
|
||||
}));
|
||||
setColumns(cols);
|
||||
} else {
|
||||
setColumns([]);
|
||||
}
|
||||
}, [props.columns, rawData]);
|
||||
|
||||
useEffect(() => {
|
||||
if (typeof props.pageSize === 'number') {
|
||||
dataController.setPageSize(props.pageSize);
|
||||
}
|
||||
}, [props.pageSize]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!!props.customSort) {
|
||||
setCustomSort(() => props.customSort!);
|
||||
}
|
||||
}, [props.customSort]);
|
||||
|
||||
useEffect(() => {
|
||||
if (props.searchKeys) {
|
||||
const propsSearchKeys = JSON.stringify(props.searchKeys);
|
||||
const controllerSearchKeys = JSON.stringify(dataController.searchKeys);
|
||||
if (propsSearchKeys !== controllerSearchKeys) {
|
||||
dataController.setSearchKeys(props.searchKeys);
|
||||
}
|
||||
} else if (columns.length > 0 && !dataController.searchKeys) {
|
||||
const colFields = columns.map((col) => col.field).filter((f): f is keyof T => !!f);
|
||||
dataController.setSearchKeys(colFields);
|
||||
}
|
||||
}, [props.searchKeys, columns.length]);
|
||||
|
||||
if (!rawData) return null;
|
||||
|
||||
if (rawData.length === 0) {
|
||||
return (
|
||||
<div className="py-12 text-center">
|
||||
<div className="text-muted-foreground">{noDataMessage || 'No Data Found.'}</div>
|
||||
{noDataCTA && <>{noDataCTA}</>}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="bg-card overflow-hidden rounded-xl border">
|
||||
<div>
|
||||
<Table>
|
||||
<TableHeader className="bg-muted/50">
|
||||
<TableRow>
|
||||
{columns.map((column: Column<T>, idx: number) => {
|
||||
const { label = '', sortKey, field, condition = true } = column;
|
||||
const { tooltip, headerClassName, labelMobile } = column;
|
||||
const renderColumn = typeof condition === 'function' ? condition() : condition;
|
||||
return (
|
||||
renderColumn && (
|
||||
<TableHead className={cn('whitespace-nowrap max-[499px]:px-2', headerClassName)} key={idx}>
|
||||
<TooltipProvider>
|
||||
<Tooltip>
|
||||
<TooltipTrigger asChild>
|
||||
<div
|
||||
className={cn(
|
||||
'flex items-center',
|
||||
sortKey && 'cursor-pointer hover:text-black',
|
||||
sortedBy?.startsWith(`${String(sortKey)}-`) && 'text-black',
|
||||
)}
|
||||
onClick={() => sortBy(sortKey as Extract<keyof T, string> | undefined)}
|
||||
>
|
||||
<span className="hidden min-[500px]:inline">
|
||||
{String(label || field || '')} {tooltip ? <sup>ⓘ</sup> : ''}
|
||||
</span>
|
||||
<span className="min-[500px]:hidden">
|
||||
{String(labelMobile || label || field || '')} {tooltip ? <sup>ⓘ</sup> : ''}
|
||||
</span>
|
||||
{sortKey && <ArrowUpDown className={cn('ml-2 h-4 w-4')} />}
|
||||
</div>
|
||||
</TooltipTrigger>
|
||||
{tooltip && (
|
||||
<TooltipContent>
|
||||
<p className="whitespace-pre-line">{tooltip}</p>
|
||||
</TooltipContent>
|
||||
)}
|
||||
</Tooltip>
|
||||
</TooltipProvider>
|
||||
</TableHead>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{data?.map((item: T, idx: number) => {
|
||||
return (
|
||||
<TableRow key={String(item[theUniqueKey as keyof T] || idx)} className="group/tablerow">
|
||||
{columns.map((column, idx) => {
|
||||
const value = column.field !== undefined ? item[column.field] : undefined;
|
||||
const { format, condition = true, cellClassName } = column;
|
||||
const renderColumn = typeof condition === 'function' ? condition() : condition;
|
||||
return (
|
||||
renderColumn && (
|
||||
<TableCell className={cn('whitespace-nowrap max-[499px]:px-2', cellClassName)} key={idx}>
|
||||
<>
|
||||
{format
|
||||
? column.field !== undefined
|
||||
? format({
|
||||
value: item[column.field],
|
||||
item,
|
||||
data,
|
||||
idx,
|
||||
})
|
||||
: format({
|
||||
value: undefined as unknown as T[keyof T],
|
||||
item,
|
||||
data,
|
||||
idx,
|
||||
})
|
||||
: value}
|
||||
</>
|
||||
</TableCell>
|
||||
)
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
{!!pageSize && pageSize < rawData?.length && (
|
||||
<PaginationBar dataManager={dataController as UseDataControlReturn<T>} />
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export { useDataControl };
|
||||
export type { UseDataControlReturn };
|
||||
|
||||
type DataTableProps<T> = {
|
||||
dataController: UseDataControlReturn<T>;
|
||||
columns?: Column<T>[];
|
||||
uniqueKey?: keyof T;
|
||||
searchKeys?: (keyof T)[];
|
||||
pageSize?: number;
|
||||
noDataCTA?: React.ReactNode;
|
||||
customSort?: (a: T, b: T, originalSort: (a: T, b: T) => number) => number;
|
||||
noDataMessage?: string;
|
||||
};
|
||||
|
||||
export type DataTableFormatArgs<T> = {
|
||||
value: T[keyof T];
|
||||
item: T;
|
||||
data: T[];
|
||||
idx: number;
|
||||
};
|
||||
|
||||
export type Column<T> = {
|
||||
field?: keyof T;
|
||||
label?: string;
|
||||
labelMobile?: string;
|
||||
headerClassName?: string;
|
||||
cellClassName?: string;
|
||||
format?: (args: DataTableFormatArgs<T>) => React.ReactNode;
|
||||
condition?: boolean | (() => boolean);
|
||||
sortKey?: SortKey<T>;
|
||||
tooltip?: string;
|
||||
};
|
||||
|
||||
function toLabel(key: string): string {
|
||||
return key
|
||||
.replace(/([a-z0-9])([A-Z])/g, '$1 $2') // insert space before capitals
|
||||
.replace(/_/g, ' ') // replace underscores with spaces
|
||||
.replace(/\s+/g, ' ') // collapse multiple spaces
|
||||
.trim()
|
||||
.replace(/\w\S*/g, (word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase());
|
||||
}
|
||||
Reference in New Issue
Block a user