first commit
This commit is contained in:
116
calcom/packages/ui/components/data-table/DataTableFilter.tsx
Normal file
116
calcom/packages/ui/components/data-table/DataTableFilter.tsx
Normal file
@@ -0,0 +1,116 @@
|
||||
import type { Column } from "@tanstack/react-table";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
|
||||
import { classNames } from "@calcom/lib";
|
||||
|
||||
import { Icon } from "../..";
|
||||
import { Badge } from "../badge";
|
||||
import { Button } from "../button";
|
||||
import {
|
||||
Command,
|
||||
CommandEmpty,
|
||||
CommandGroup,
|
||||
CommandInput,
|
||||
CommandItem,
|
||||
CommandList,
|
||||
CommandSeparator,
|
||||
} from "../command";
|
||||
import { Popover, PopoverContent, PopoverTrigger } from "../popover";
|
||||
|
||||
interface DataTableFilter<TData, TValue> {
|
||||
column?: Column<TData, TValue>;
|
||||
title?: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: LucideIcon;
|
||||
}[];
|
||||
}
|
||||
export function DataTableFilter<TData, TValue>({ column, title, options }: DataTableFilter<TData, TValue>) {
|
||||
const facets = column?.getFacetedUniqueValues();
|
||||
const selectedValues = new Set(column?.getFilterValue() as string[]);
|
||||
|
||||
return (
|
||||
<Popover>
|
||||
<PopoverTrigger asChild>
|
||||
<Button color="secondary" size="sm" className="border-subtle h-8 rounded-md">
|
||||
<Icon name="filter" className="mr-2 h-4 w-4" />
|
||||
{title}
|
||||
{selectedValues?.size > 0 && (
|
||||
<>
|
||||
<div className="ml-2 hidden space-x-1 md:flex">
|
||||
{selectedValues.size > 2 ? (
|
||||
<Badge color="gray" className="rounded-sm px-1 font-normal">
|
||||
{selectedValues.size}
|
||||
</Badge>
|
||||
) : (
|
||||
options
|
||||
.filter((option) => selectedValues.has(option.value))
|
||||
.map((option) => (
|
||||
<Badge color="gray" key={option.value} className="rounded-sm px-1 font-normal">
|
||||
{option.label}
|
||||
</Badge>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
</PopoverTrigger>
|
||||
<PopoverContent className="w-[200px] p-0" align="start">
|
||||
<Command>
|
||||
<CommandInput placeholder={title} />
|
||||
<CommandList>
|
||||
<CommandEmpty>No results found.</CommandEmpty>
|
||||
<CommandGroup>
|
||||
{options.map((option) => {
|
||||
// TODO: It would be nice to pull these from data instead of options
|
||||
const isSelected = selectedValues.has(option.value);
|
||||
return (
|
||||
<CommandItem
|
||||
key={option.value}
|
||||
onSelect={() => {
|
||||
if (isSelected) {
|
||||
selectedValues.delete(option.value);
|
||||
} else {
|
||||
selectedValues.add(option.value);
|
||||
}
|
||||
const filterValues = Array.from(selectedValues);
|
||||
column?.setFilterValue(filterValues.length ? filterValues : undefined);
|
||||
}}>
|
||||
<div
|
||||
className={classNames(
|
||||
"border-subtle mr-2 flex h-4 w-4 items-center justify-center rounded-sm border",
|
||||
isSelected ? "text-emphasis" : "opacity-50 [&_svg]:invisible"
|
||||
)}>
|
||||
<Icon name="check" className={classNames("h-4 w-4")} />
|
||||
</div>
|
||||
{option.icon && <option.icon className="text-muted mr-2 h-4 w-4" />}
|
||||
<span>{option.label}</span>
|
||||
{facets?.get(option.value) && (
|
||||
<span className="ml-auto flex h-4 w-4 items-center justify-center font-mono text-xs">
|
||||
{facets.get(option.value)}
|
||||
</span>
|
||||
)}
|
||||
</CommandItem>
|
||||
);
|
||||
})}
|
||||
</CommandGroup>
|
||||
{selectedValues.size > 0 && (
|
||||
<>
|
||||
<CommandSeparator />
|
||||
<CommandGroup>
|
||||
<CommandItem
|
||||
onSelect={() => column?.setFilterValue(undefined)}
|
||||
className="justify-center text-center">
|
||||
Clear filters
|
||||
</CommandItem>
|
||||
</CommandGroup>
|
||||
</>
|
||||
)}
|
||||
</CommandList>
|
||||
</Command>
|
||||
</PopoverContent>
|
||||
</Popover>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
|
||||
import { Button } from "../button";
|
||||
|
||||
interface DataTablePaginationProps<TData> {
|
||||
table: Table<TData>;
|
||||
}
|
||||
|
||||
export function DataTablePagination<TData>({ table }: DataTablePaginationProps<TData>) {
|
||||
return (
|
||||
<div className="flex items-center justify-between px-2">
|
||||
<div className="text-muted-foreground flex-1 text-sm">
|
||||
{table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s)
|
||||
selected.
|
||||
</div>
|
||||
<div className="flex items-center space-x-6 lg:space-x-8">
|
||||
<div className="flex w-[100px] items-center justify-center text-sm font-medium">
|
||||
Page {table.getState().pagination.pageIndex} of {table.getPageCount() - 1}
|
||||
</div>
|
||||
<div className="flex items-center space-x-2">
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="chevrons-left"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
onClick={() => table.setPageIndex(0)}>
|
||||
<span className="sr-only">Go to first page</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
className="h-8 w-8 p-0"
|
||||
onClick={() => table.previousPage()}
|
||||
disabled={!table.getCanPreviousPage()}
|
||||
StartIcon="chevron-left">
|
||||
<span className="sr-only">Go to previous page</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
StartIcon="chevron-right"
|
||||
className="h-8 w-8 p-0"
|
||||
disabled={!table.getCanNextPage()}
|
||||
onClick={() => table.nextPage()}>
|
||||
<span className="sr-only">Go to next page</span>
|
||||
</Button>
|
||||
<Button
|
||||
color="secondary"
|
||||
variant="icon"
|
||||
className="hidden h-8 w-8 p-0 lg:flex"
|
||||
StartIcon="chevrons-right"
|
||||
onClick={() => table.setPageIndex(table.getPageCount())}>
|
||||
<span className="sr-only">Go to last page</span>
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
import type { Table as TableType } from "@tanstack/table-core/build/lib/types";
|
||||
import { AnimatePresence } from "framer-motion";
|
||||
import { Fragment } from "react";
|
||||
|
||||
import type { IconName } from "../..";
|
||||
import { Button } from "../button";
|
||||
|
||||
export type ActionItem<TData> =
|
||||
| {
|
||||
type: "action";
|
||||
label: string;
|
||||
onClick: () => void;
|
||||
icon?: IconName;
|
||||
needsXSelected?: number;
|
||||
}
|
||||
| {
|
||||
type: "render";
|
||||
render: (table: Table<TData>) => React.ReactNode;
|
||||
needsXSelected?: number;
|
||||
};
|
||||
|
||||
interface DataTableSelectionBarProps<TData> {
|
||||
table: Table<TData>;
|
||||
actions?: ActionItem<TData>[];
|
||||
renderAboveSelection?: (table: TableType<TData>) => React.ReactNode;
|
||||
}
|
||||
|
||||
export function DataTableSelectionBar<TData>({
|
||||
table,
|
||||
actions,
|
||||
renderAboveSelection,
|
||||
}: DataTableSelectionBarProps<TData>) {
|
||||
const numberOfSelectedRows = table.getSelectedRowModel().rows.length;
|
||||
const isVisible = numberOfSelectedRows > 0;
|
||||
|
||||
// Hacky left % to center
|
||||
const actionsVisible = actions?.filter((a) => {
|
||||
if (!a.needsXSelected) return true;
|
||||
return a.needsXSelected <= numberOfSelectedRows;
|
||||
});
|
||||
|
||||
return (
|
||||
<AnimatePresence>
|
||||
{isVisible ? (
|
||||
<div className="fade-in fixed bottom-6 left-1/2 hidden -translate-x-1/2 gap-1 md:flex md:flex-col">
|
||||
{renderAboveSelection && renderAboveSelection(table)}
|
||||
<div className="bg-brand-default text-brand hidden items-center justify-between rounded-lg p-2 md:flex">
|
||||
<p className="text-brand-subtle w-full px-2 text-center leading-none">
|
||||
{numberOfSelectedRows} selected
|
||||
</p>
|
||||
{actionsVisible?.map((action, index) => {
|
||||
return (
|
||||
<Fragment key={index}>
|
||||
{action.type === "action" ? (
|
||||
<Button aria-label={action.label} onClick={action.onClick} StartIcon={action.icon}>
|
||||
{action.label}
|
||||
</Button>
|
||||
) : action.type === "render" ? (
|
||||
action.render(table)
|
||||
) : null}
|
||||
</Fragment>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
) : null}
|
||||
</AnimatePresence>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,99 @@
|
||||
"use client";
|
||||
|
||||
import type { Table } from "@tanstack/react-table";
|
||||
import type { LucideIcon } from "lucide-react";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
import { useDebounce } from "@calcom/lib/hooks/useDebounce";
|
||||
import { useLocale } from "@calcom/lib/hooks/useLocale";
|
||||
|
||||
import { Button } from "../button";
|
||||
import { Input } from "../form";
|
||||
import { DataTableFilter } from "./DataTableFilter";
|
||||
|
||||
export type FilterableItems = {
|
||||
title: string;
|
||||
tableAccessor: string;
|
||||
options: {
|
||||
label: string;
|
||||
value: string;
|
||||
icon?: LucideIcon;
|
||||
}[];
|
||||
}[];
|
||||
|
||||
interface DataTableToolbarProps<TData> {
|
||||
table: Table<TData>;
|
||||
filterableItems?: FilterableItems;
|
||||
searchKey?: string;
|
||||
tableCTA?: React.ReactNode;
|
||||
onSearch?: (value: string) => void;
|
||||
}
|
||||
|
||||
export function DataTableToolbar<TData>({
|
||||
table,
|
||||
filterableItems,
|
||||
tableCTA,
|
||||
searchKey,
|
||||
onSearch,
|
||||
}: DataTableToolbarProps<TData>) {
|
||||
// TODO: Is there a better way to check if the table is filtered?
|
||||
// If you select ALL filters for a column, the table is not filtered and we dont get a reset button
|
||||
const isFiltered = table.getState().columnFilters.length > 0;
|
||||
const [searchTerm, setSearchTerm] = useState("");
|
||||
const debouncedSearchTerm = useDebounce(searchTerm, 500);
|
||||
|
||||
useEffect(() => {
|
||||
onSearch?.(debouncedSearchTerm);
|
||||
}, [debouncedSearchTerm, onSearch]);
|
||||
|
||||
const { t } = useLocale();
|
||||
|
||||
return (
|
||||
<div className="flex items-center justify-end py-4">
|
||||
{searchKey && (
|
||||
<Input
|
||||
className="max-w-64 mb-0 mr-auto rounded-md"
|
||||
placeholder="Search"
|
||||
value={(table.getColumn(searchKey)?.getFilterValue() as string) ?? ""}
|
||||
onChange={(event) => table.getColumn(searchKey)?.setFilterValue(event.target.value.trim())}
|
||||
/>
|
||||
)}
|
||||
{onSearch && (
|
||||
<Input
|
||||
className="max-w-64 mb-0 mr-auto rounded-md"
|
||||
placeholder="Search"
|
||||
value={searchTerm}
|
||||
onChange={(e) => {
|
||||
setSearchTerm(e.target.value);
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{isFiltered && (
|
||||
<Button
|
||||
color="minimal"
|
||||
EndIcon="x"
|
||||
onClick={() => table.resetColumnFilters()}
|
||||
className="h-8 px-2 lg:px-3">
|
||||
{t("clear")}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{filterableItems &&
|
||||
filterableItems?.map((item) => {
|
||||
const foundColumn = table.getColumn(item.tableAccessor);
|
||||
if (foundColumn?.getCanFilter()) {
|
||||
return (
|
||||
<DataTableFilter
|
||||
column={foundColumn}
|
||||
title={item.title}
|
||||
options={item.options}
|
||||
key={item.title}
|
||||
/>
|
||||
);
|
||||
}
|
||||
})}
|
||||
|
||||
{tableCTA ? tableCTA : null}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
import type { ColumnDef } from "@tanstack/react-table";
|
||||
|
||||
import { Badge } from "../../badge";
|
||||
import { Checkbox } from "../../form";
|
||||
import type { FilterableItems } from "../DataTableToolbar";
|
||||
import type { DataTableUserStorybook } from "./data";
|
||||
|
||||
export const columns: ColumnDef<DataTableUserStorybook>[] = [
|
||||
{
|
||||
id: "select",
|
||||
header: ({ table }) => (
|
||||
<Checkbox
|
||||
checked={table.getIsAllPageRowsSelected()}
|
||||
onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)}
|
||||
aria-label="Select all"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
cell: ({ row }) => (
|
||||
<Checkbox
|
||||
checked={row.getIsSelected()}
|
||||
onCheckedChange={(value) => row.toggleSelected(!!value)}
|
||||
aria-label="Select row"
|
||||
className="translate-y-[2px]"
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
accessorKey: "username",
|
||||
header: "Username",
|
||||
},
|
||||
{
|
||||
accessorKey: "email",
|
||||
header: "Email",
|
||||
},
|
||||
{
|
||||
accessorKey: "role",
|
||||
header: "Role",
|
||||
cell: ({ row, table }) => {
|
||||
const user = row.original;
|
||||
const BadgeColor = user.role === "admin" ? "blue" : "gray";
|
||||
|
||||
return (
|
||||
<Badge
|
||||
color={BadgeColor}
|
||||
onClick={() => {
|
||||
table.getColumn("role")?.setFilterValue(user.role);
|
||||
}}>
|
||||
{user.role}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
filterFn: (rows, id, filterValue) => {
|
||||
return filterValue.includes(rows.getValue(id));
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
export const filterableItems: FilterableItems = [
|
||||
{
|
||||
title: "Role",
|
||||
tableAccessor: "role",
|
||||
options: [
|
||||
{
|
||||
label: "Admin",
|
||||
value: "admin",
|
||||
},
|
||||
{
|
||||
label: "User",
|
||||
value: "user",
|
||||
},
|
||||
{
|
||||
label: "Owner",
|
||||
value: "owner",
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,98 @@
|
||||
export type DataTableUserStorybook = {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
role: "admin" | "user";
|
||||
};
|
||||
|
||||
export const dataTableSelectionActions = [
|
||||
{
|
||||
label: "Add To Team",
|
||||
onClick: () => {
|
||||
console.log("Add To Team");
|
||||
},
|
||||
icon: "users",
|
||||
},
|
||||
{
|
||||
label: "Delete",
|
||||
onClick: () => {
|
||||
console.log("Delete");
|
||||
},
|
||||
icon: "stop-circle",
|
||||
},
|
||||
];
|
||||
|
||||
export const dataTableDemousers: DataTableUserStorybook[] = [
|
||||
{
|
||||
id: "728ed52f",
|
||||
email: "m@example.com",
|
||||
username: "m",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
id: "489e1d42",
|
||||
email: "example@gmail.com",
|
||||
username: "e",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "7b8a6f1d-2d2d-4d29-9c1a-0a8b3f5f9d2f",
|
||||
email: "Keshawn_Schroeder@hotmail.com",
|
||||
username: "Ava_Waelchi",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "f4d9e2a3-7e3c-4d6e-8e4c-8d0d7d1c2c9b",
|
||||
email: "Jovanny_Grant@hotmail.com",
|
||||
username: "Kamren_Gerhold",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
id: "1b2a4b6e-5b2d-4c38-9c7e-9d5e8f9c0a6a",
|
||||
email: "Emilie.McKenzie@yahoo.com",
|
||||
username: "Lennie_Harber",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "d6f3e6e9-9c2a-4c8a-8f3c-0d63a0eaf5a5",
|
||||
email: "Jolie_Beatty@hotmail.com",
|
||||
username: "Lorenzo_Will",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
id: "7c1e5d1d-8b9c-4b1c-9d1b-7d9f8b5a7e3e",
|
||||
email: "Giovanny_Cruickshank@hotmail.com",
|
||||
username: "Monserrat_Lang",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "f7d8b7a2-0a5c-4f8d-9f4f-8d1a2c3e4b3e",
|
||||
email: "Lela_Haag@hotmail.com",
|
||||
username: "Eddie_Effertz",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "2f8b9c8d-1a5c-4e3d-9b7a-6c5d4e3f2b1a",
|
||||
email: "Lura_Kohler@gmail.com",
|
||||
username: "Alyce_Olson",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "d8c7b6a5-4e3d-2b1a-9c8d-1f2e3d4c5b6a",
|
||||
email: "Maurice.Koch@hotmail.com",
|
||||
username: "Jovanny_Kiehn",
|
||||
role: "admin",
|
||||
},
|
||||
{
|
||||
id: "3c2b1a5d-4e3d-8c7b-9a6f-0d1e2f3g4h5i",
|
||||
email: "Brenda_Bernhard@yahoo.com",
|
||||
username: "Aurelia_Kemmer",
|
||||
role: "user",
|
||||
},
|
||||
{
|
||||
id: "e4d3c2b1-5e4d-3c2b-1a9f-8g7h6i5j4k3l",
|
||||
email: "Lorenzo_Rippin@hotmail.com",
|
||||
username: "Waino_Lang",
|
||||
role: "admin",
|
||||
},
|
||||
];
|
||||
@@ -0,0 +1,67 @@
|
||||
import { Canvas, Meta, Story, ArgsTable } from "@storybook/addon-docs";
|
||||
|
||||
import {
|
||||
Examples,
|
||||
Example,
|
||||
Note,
|
||||
Title,
|
||||
CustomArgsTable,
|
||||
VariantsTable,
|
||||
VariantRow,
|
||||
} from "@calcom/storybook/components";
|
||||
|
||||
import { DataTable } from "../";
|
||||
import { columns, filterableItems } from "./columns";
|
||||
import { dataTableDemousers, dataTableSelectionActions } from "./data";
|
||||
|
||||
<Meta title="UI/table/DataTable" component={DataTable} />
|
||||
|
||||
<Title title="DataTable" suffix="Brief" subtitle="Version 3.0 — Last Update: 28 Aug 2023" />
|
||||
|
||||
## Definition
|
||||
|
||||
The `DataTable` component facilitates tabular data display with configurable columns, virtual scrolling, filtering, and interactive features for seamless dynamic table creation.
|
||||
|
||||
## Structure
|
||||
|
||||
The `DataTable` setup for tabular data, with columns, virtual scroll, sticky headers, and interactive features like filtering and row selection.
|
||||
|
||||
<CustomArgsTable of={DataTable} />
|
||||
|
||||
## Dialog Story
|
||||
|
||||
<Canvas>
|
||||
<Story
|
||||
name="DataTable"
|
||||
args={{
|
||||
columns: columns,
|
||||
data: dataTableDemousers,
|
||||
isPending: false,
|
||||
searchKey: "username",
|
||||
filterableItems: filterableItems,
|
||||
tableContainerRef: { current: null },
|
||||
selectionOptions: dataTableSelectionActions,
|
||||
}}
|
||||
argTypes={{
|
||||
tableContainerRef: { table: { disable: true } },
|
||||
searchKey: {
|
||||
control: {
|
||||
type: "select",
|
||||
options: ["username", "email"],
|
||||
},
|
||||
},
|
||||
selectionOptions: { control: { type: "object" } },
|
||||
onScroll: { table: { disable: true } },
|
||||
tableOverlay: { table: { disable: true } },
|
||||
CTA: { table: { disable: true } },
|
||||
tableCTA: { table: { disable: true } },
|
||||
}}>
|
||||
{(args) => (
|
||||
<VariantsTable titles={["Default"]} columnMinWidth={1000}>
|
||||
<VariantRow>
|
||||
<DataTable {...args} />
|
||||
</VariantRow>
|
||||
</VariantsTable>
|
||||
)}
|
||||
</Story>
|
||||
</Canvas>
|
||||
184
calcom/packages/ui/components/data-table/index.tsx
Normal file
184
calcom/packages/ui/components/data-table/index.tsx
Normal file
@@ -0,0 +1,184 @@
|
||||
import type {
|
||||
ColumnDef,
|
||||
ColumnFiltersState,
|
||||
Row,
|
||||
SortingState,
|
||||
VisibilityState,
|
||||
Table as TableType,
|
||||
} from "@tanstack/react-table";
|
||||
import {
|
||||
flexRender,
|
||||
getCoreRowModel,
|
||||
getFacetedRowModel,
|
||||
getFacetedUniqueValues,
|
||||
getFilteredRowModel,
|
||||
getSortedRowModel,
|
||||
useReactTable,
|
||||
} from "@tanstack/react-table";
|
||||
import { useState } from "react";
|
||||
import { useVirtual } from "react-virtual";
|
||||
|
||||
import classNames from "@calcom/lib/classNames";
|
||||
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "../table/TableNew";
|
||||
import type { ActionItem } from "./DataTableSelectionBar";
|
||||
import { DataTableSelectionBar } from "./DataTableSelectionBar";
|
||||
import type { FilterableItems } from "./DataTableToolbar";
|
||||
import { DataTableToolbar } from "./DataTableToolbar";
|
||||
|
||||
export interface DataTableProps<TData, TValue> {
|
||||
tableContainerRef: React.RefObject<HTMLDivElement>;
|
||||
columns: ColumnDef<TData, TValue>[];
|
||||
data: TData[];
|
||||
searchKey?: string;
|
||||
onSearch?: (value: string) => void;
|
||||
filterableItems?: FilterableItems;
|
||||
selectionOptions?: ActionItem<TData>[];
|
||||
renderAboveSelection?: (table: TableType<TData>) => React.ReactNode;
|
||||
tableCTA?: React.ReactNode;
|
||||
isPending?: boolean;
|
||||
onRowMouseclick?: (row: Row<TData>) => void;
|
||||
onScroll?: (e: React.UIEvent<HTMLDivElement, UIEvent>) => void;
|
||||
CTA?: React.ReactNode;
|
||||
tableOverlay?: React.ReactNode;
|
||||
variant?: "default" | "compact";
|
||||
"data-testId"?: string;
|
||||
}
|
||||
|
||||
export function DataTable<TData, TValue>({
|
||||
columns,
|
||||
data,
|
||||
filterableItems,
|
||||
tableCTA,
|
||||
searchKey,
|
||||
selectionOptions,
|
||||
tableContainerRef,
|
||||
isPending,
|
||||
tableOverlay,
|
||||
variant,
|
||||
renderAboveSelection,
|
||||
/** This should only really be used if you dont have actions in a row. */
|
||||
onSearch,
|
||||
onRowMouseclick,
|
||||
onScroll,
|
||||
...rest
|
||||
}: DataTableProps<TData, TValue>) {
|
||||
const [rowSelection, setRowSelection] = useState({});
|
||||
const [columnVisibility, setColumnVisibility] = useState<VisibilityState>({});
|
||||
const [columnFilters, setColumnFilters] = useState<ColumnFiltersState>([]);
|
||||
const [sorting, setSorting] = useState<SortingState>([]);
|
||||
|
||||
const table = useReactTable({
|
||||
data,
|
||||
columns,
|
||||
state: {
|
||||
sorting,
|
||||
columnVisibility,
|
||||
rowSelection,
|
||||
columnFilters,
|
||||
},
|
||||
enableRowSelection: true,
|
||||
debugTable: true,
|
||||
manualPagination: true,
|
||||
onRowSelectionChange: setRowSelection,
|
||||
onSortingChange: setSorting,
|
||||
onColumnFiltersChange: setColumnFilters,
|
||||
onColumnVisibilityChange: setColumnVisibility,
|
||||
getCoreRowModel: getCoreRowModel(),
|
||||
getFilteredRowModel: getFilteredRowModel(),
|
||||
getSortedRowModel: getSortedRowModel(),
|
||||
getFacetedRowModel: getFacetedRowModel(),
|
||||
getFacetedUniqueValues: getFacetedUniqueValues(),
|
||||
});
|
||||
|
||||
const { rows } = table.getRowModel();
|
||||
|
||||
const rowVirtualizer = useVirtual({
|
||||
parentRef: tableContainerRef,
|
||||
size: rows.length,
|
||||
overscan: 10,
|
||||
});
|
||||
const { virtualItems: virtualRows, totalSize } = rowVirtualizer;
|
||||
const paddingTop = virtualRows.length > 0 ? virtualRows?.[0]?.start || 0 : 0;
|
||||
const paddingBottom =
|
||||
virtualRows.length > 0 ? totalSize - (virtualRows?.[virtualRows.length - 1]?.end || 0) : 0;
|
||||
|
||||
return (
|
||||
<div className="relative space-y-4">
|
||||
<DataTableToolbar
|
||||
table={table}
|
||||
filterableItems={filterableItems}
|
||||
searchKey={searchKey}
|
||||
onSearch={onSearch}
|
||||
tableCTA={tableCTA}
|
||||
/>
|
||||
<div ref={tableContainerRef} onScroll={onScroll} data-testId={rest["data-testId"] ?? "data-table"}>
|
||||
<Table data-testId="">
|
||||
<TableHeader>
|
||||
{table.getHeaderGroups().map((headerGroup) => (
|
||||
<TableRow key={headerGroup.id}>
|
||||
{headerGroup.headers.map((header) => {
|
||||
return (
|
||||
<TableHead key={header.id}>
|
||||
{header.isPlaceholder
|
||||
? null
|
||||
: flexRender(header.column.columnDef.header, header.getContext())}
|
||||
</TableHead>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
))}
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{paddingTop > 0 && (
|
||||
<tr>
|
||||
<td style={{ height: `${paddingTop}px` }} />
|
||||
</tr>
|
||||
)}
|
||||
{virtualRows && !isPending ? (
|
||||
virtualRows.map((virtualRow) => {
|
||||
const row = rows[virtualRow.index] as Row<TData>;
|
||||
return (
|
||||
<TableRow
|
||||
key={row.id}
|
||||
data-state={row.getIsSelected() && "selected"}
|
||||
onClick={() => onRowMouseclick && onRowMouseclick(row)}
|
||||
className={classNames(
|
||||
onRowMouseclick && "hover:cursor-pointer",
|
||||
variant === "compact" && "!border-0"
|
||||
)}>
|
||||
{row.getVisibleCells().map((cell) => {
|
||||
return (
|
||||
<TableCell key={cell.id} className={classNames(variant === "compact" && "p-1.5")}>
|
||||
{flexRender(cell.column.columnDef.cell, cell.getContext())}
|
||||
</TableCell>
|
||||
);
|
||||
})}
|
||||
</TableRow>
|
||||
);
|
||||
})
|
||||
) : (
|
||||
<TableRow>
|
||||
<TableCell colSpan={columns.length} className="h-24 text-center">
|
||||
No results.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
{paddingBottom > 0 && (
|
||||
<tr>
|
||||
<td style={{ height: `${paddingBottom}px` }} />
|
||||
</tr>
|
||||
)}
|
||||
</TableBody>
|
||||
{tableOverlay && tableOverlay}
|
||||
</Table>
|
||||
</div>
|
||||
{/* <DataTablePagination table={table} /> */}
|
||||
<DataTableSelectionBar
|
||||
table={table}
|
||||
actions={selectionOptions}
|
||||
renderAboveSelection={renderAboveSelection}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user