mirror of
https://github.com/shishantbiswas/bknd.git
synced 2026-03-17 04:46:05 +00:00
public commit
This commit is contained in:
297
app/src/ui/components/table/DataTable.tsx
Normal file
297
app/src/ui/components/table/DataTable.tsx
Normal file
@@ -0,0 +1,297 @@
|
||||
import { Menu } from "@mantine/core";
|
||||
import { useToggle } from "@mantine/hooks";
|
||||
import { ucFirst } from "core/utils";
|
||||
import {
|
||||
TbArrowDown,
|
||||
TbArrowUp,
|
||||
TbChevronLeft,
|
||||
TbChevronRight,
|
||||
TbChevronsLeft,
|
||||
TbChevronsRight,
|
||||
TbDotsVertical,
|
||||
TbSelector,
|
||||
TbSquare,
|
||||
TbSquareCheckFilled
|
||||
} from "react-icons/tb";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
import { Button } from "ui/components/buttons/Button";
|
||||
import { IconButton } from "../buttons/IconButton";
|
||||
import { Dropdown, type DropdownItem } from "../overlay/Dropdown";
|
||||
|
||||
export const Check = () => {
|
||||
const [checked, toggle] = useToggle([false, true]);
|
||||
const Icon = checked ? TbSquareCheckFilled : TbSquare;
|
||||
return (
|
||||
<button role="checkbox" type="button" className="flex px-3 py-3" onClick={() => toggle()}>
|
||||
<Icon size={18} />
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
export type DataTableProps<Data> = {
|
||||
data: Data[];
|
||||
columns?: string[];
|
||||
checkable?: boolean;
|
||||
onClickRow?: (row: Data) => void;
|
||||
onClickPage?: (page: number) => void;
|
||||
total?: number;
|
||||
page?: number;
|
||||
perPage?: number;
|
||||
rowActions?: (Omit<DropdownItem, "onClick"> & {
|
||||
onClick: (row: Data, key: number) => void;
|
||||
})[];
|
||||
perPageOptions?: number[];
|
||||
sort?: { by?: string; dir?: "asc" | "desc" };
|
||||
onClickSort?: (name: string) => void;
|
||||
onClickPerPage?: (perPage: number) => void;
|
||||
renderHeader?: (column: string) => React.ReactNode;
|
||||
renderValue?: ({ value, property }: { value: any; property: string }) => React.ReactNode;
|
||||
classNames?: {
|
||||
value?: string;
|
||||
};
|
||||
onClickNew?: () => void;
|
||||
};
|
||||
|
||||
export function DataTable<Data extends Record<string, any> = Record<string, any>>({
|
||||
data = [],
|
||||
columns,
|
||||
checkable,
|
||||
onClickRow,
|
||||
onClickPage,
|
||||
onClickSort,
|
||||
total,
|
||||
sort,
|
||||
page = 1,
|
||||
perPage = 10,
|
||||
perPageOptions,
|
||||
onClickPerPage,
|
||||
classNames,
|
||||
renderHeader,
|
||||
rowActions,
|
||||
renderValue,
|
||||
onClickNew
|
||||
}: DataTableProps<Data>) {
|
||||
total = total || data.length;
|
||||
page = page || 1;
|
||||
|
||||
const select = columns && columns.length > 0 ? columns : Object.keys(data[0] || {});
|
||||
const pages = Math.max(Math.ceil(total / perPage), 1);
|
||||
const CellRender = renderValue || CellValue;
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-3">
|
||||
{onClickNew && (
|
||||
<div className="flex flex-row space-between">
|
||||
{onClickNew && <Button onClick={onClickNew}>Create new</Button>}
|
||||
</div>
|
||||
)}
|
||||
<div className="border-muted border rounded-md shadow-sm w-full max-w-full overflow-x-scroll overflow-y-hidden">
|
||||
<table className="w-full">
|
||||
{select.length > 0 ? (
|
||||
<thead className="sticky top-0 bg-muted/10">
|
||||
<tr>
|
||||
{checkable && (
|
||||
<th align="center" className="w-[40px]">
|
||||
<Check />
|
||||
</th>
|
||||
)}
|
||||
{select.map((property, key) => {
|
||||
const label = renderHeader?.(property) ?? ucFirst(property);
|
||||
|
||||
return (
|
||||
<th key={key}>
|
||||
<div className="flex flex-row py-1 px-1 font-normal text-primary/55">
|
||||
<button
|
||||
type="button"
|
||||
className={twMerge(
|
||||
"link hover:bg-primary/5 py-1.5 rounded-md inline-flex flex-row justify-start items-center gap-1",
|
||||
onClickSort ? "pl-2.5 pr-1" : "px-2.5"
|
||||
)}
|
||||
onClick={() => onClickSort?.(property)}
|
||||
>
|
||||
<span className="text-left text-nowrap whitespace-nowrap">
|
||||
{label}
|
||||
</span>
|
||||
{onClickSort && (
|
||||
<SortIndicator sort={sort} field={property} />
|
||||
)}
|
||||
</button>
|
||||
</div>
|
||||
</th>
|
||||
);
|
||||
})}
|
||||
{rowActions && rowActions.length > 0 && <th className="w-10" />}
|
||||
</tr>
|
||||
</thead>
|
||||
) : null}
|
||||
<tbody>
|
||||
{!data || data.length === 0 ? (
|
||||
<tr>
|
||||
<td colSpan={select.length + (checkable ? 1 : 0)}>
|
||||
<div className="flex flex-col gap-2 p-8 justify-center items-center border-t border-muted">
|
||||
<i className="opacity-50">No data to show</i>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
) : (
|
||||
data.map((row, key) => {
|
||||
const rowClick = () => onClickRow?.(row);
|
||||
return (
|
||||
<tr
|
||||
key={key}
|
||||
data-border={key > 0}
|
||||
className="hover:bg-primary/5 active:bg-muted border-muted data-[border]:border-t cursor-pointer transition-colors"
|
||||
>
|
||||
{checkable && (
|
||||
<td align="center">
|
||||
<Check />
|
||||
</td>
|
||||
)}
|
||||
|
||||
{Object.entries(row).map(([key, value], index) => (
|
||||
<td key={index} onClick={rowClick}>
|
||||
<div className="flex flex-row items-start py-3 px-3.5 font-normal ">
|
||||
<CellRender property={key} value={value} />
|
||||
</div>
|
||||
</td>
|
||||
))}
|
||||
|
||||
{rowActions && rowActions.length > 0 && (
|
||||
<td>
|
||||
{/* @todo: create new dropdown using popover */}
|
||||
<div className="flex flex-row justify-end pr-2">
|
||||
<Menu position="bottom-end">
|
||||
<Menu.Target>
|
||||
<IconButton Icon={TbDotsVertical} />
|
||||
</Menu.Target>
|
||||
<Menu.Dropdown>
|
||||
{rowActions.map((a: any) => (
|
||||
<Menu.Item
|
||||
key={a.label}
|
||||
onClick={() => a.onClick(row, key)}
|
||||
leftSection={a.icon && <a.icon />}
|
||||
>
|
||||
{a.label}
|
||||
</Menu.Item>
|
||||
))}
|
||||
</Menu.Dropdown>
|
||||
</Menu>
|
||||
</div>
|
||||
</td>
|
||||
)}
|
||||
</tr>
|
||||
);
|
||||
})
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<div className="flex flex-row items-center justify-between">
|
||||
<div className="hidden md:flex text-primary/40">
|
||||
<TableDisplay perPage={perPage} page={page} items={data.length} total={total} />
|
||||
</div>
|
||||
<div className="flex flex-row gap-2 md:gap-10 items-center">
|
||||
{perPageOptions && (
|
||||
<div className="hidden md:flex flex-row items-center gap-2 text-primary/40">
|
||||
Per Page{" "}
|
||||
<Dropdown
|
||||
items={perPageOptions.map((perPage) => ({
|
||||
label: String(perPage),
|
||||
perPage
|
||||
}))}
|
||||
position="top-end"
|
||||
onClickItem={(item: any) => onClickPerPage?.(item.perPage)}
|
||||
>
|
||||
<Button>{perPage}</Button>
|
||||
</Dropdown>
|
||||
</div>
|
||||
)}
|
||||
<div className="text-primary/40">
|
||||
Page {page} of {pages}
|
||||
</div>
|
||||
{onClickPage && (
|
||||
<div className="flex flex-row gap-1.5">
|
||||
<TableNav current={page} total={pages} onClick={onClickPage} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
export const CellValue = ({ value, property }) => {
|
||||
let value_mono = false;
|
||||
//console.log("value", property, value);
|
||||
if (value !== null && typeof value === "object") {
|
||||
value = JSON.stringify(value);
|
||||
value_mono = true;
|
||||
}
|
||||
|
||||
if (value !== null && typeof value !== "undefined") {
|
||||
return <span className={twMerge("line-clamp-2", value_mono && "font-mono")}>{value}</span>;
|
||||
}
|
||||
|
||||
return <span className="opacity-10 font-mono">null</span>;
|
||||
};
|
||||
|
||||
const SortIndicator = ({
|
||||
sort,
|
||||
field
|
||||
}: {
|
||||
sort: Pick<DataTableProps<any>, "sort">["sort"];
|
||||
field: string;
|
||||
}) => {
|
||||
if (!sort || sort.by !== field) return <TbSelector size={18} className="mt-[1px]" />;
|
||||
|
||||
if (sort.dir === "asc") return <TbArrowUp size={18} className="mt-[1px]" />;
|
||||
return <TbArrowDown size={18} className="mt-[1px]" />;
|
||||
};
|
||||
|
||||
const TableDisplay = ({ perPage, page, items, total }) => {
|
||||
if (total === 0) {
|
||||
return <>No rows to show</>;
|
||||
}
|
||||
|
||||
if (total === 1) {
|
||||
return <>Showing 1 row</>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
Showing {perPage * (page - 1) + 1}-{perPage * (page - 1) + items} of {total} rows
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
type TableNavProps = {
|
||||
current: number;
|
||||
total: number;
|
||||
onClick?: (page: number) => void;
|
||||
};
|
||||
|
||||
const TableNav: React.FC<TableNavProps> = ({ current, total, onClick }: TableNavProps) => {
|
||||
const navMap = [
|
||||
{ value: 1, Icon: TbChevronsLeft, disabled: current === 1 },
|
||||
{ value: current - 1, Icon: TbChevronLeft, disabled: current === 1 },
|
||||
{ value: current + 1, Icon: TbChevronRight, disabled: current === total },
|
||||
{ value: total, Icon: TbChevronsRight, disabled: current === total }
|
||||
] as const;
|
||||
|
||||
return navMap.map((nav, key) => (
|
||||
<button
|
||||
role="button"
|
||||
type="button"
|
||||
key={key}
|
||||
disabled={nav.disabled}
|
||||
className="px-2 py-2 border-muted border rounded-md enabled:link text-lg enabled:hover:bg-primary/5 text-primary/90 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
onClick={() => {
|
||||
const page = nav.value;
|
||||
const safePage = page < 1 ? 1 : page > total ? total : page;
|
||||
onClick?.(safePage);
|
||||
}}
|
||||
>
|
||||
<nav.Icon />
|
||||
</button>
|
||||
));
|
||||
};
|
||||
Reference in New Issue
Block a user