Skip to content

Commit 621a79e

Browse files
authored
port DataTable functionality from Pro (#102)
* port DataTable functionality from Pro * implement customized DataTable story * create mechanism to inject icons * showcase sorting & custom icon injection
1 parent 326d2de commit 621a79e

File tree

9 files changed

+324
-18
lines changed

9 files changed

+324
-18
lines changed

.changeset/yummy-ends-knock.md

+5
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
---
2+
"@zenml-io/react-component-library": minor
3+
---
4+
5+
port full functionality of DataTable component from pro

src/components/Table/DataTable.stories.tsx

+96-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
import { Meta } from "@storybook/react";
22
import { StoryObj } from "@storybook/react";
3-
import React from "react";
4-
import { DataTable } from "./index";
5-
import { ColumnDef } from "@tanstack/react-table";
3+
import React, { useState } from "react";
4+
import { DataTable, injectSortingArrowIcons } from "./index";
5+
import { ColumnDef, RowSelectionState, SortingState } from "@tanstack/react-table";
6+
import { Checkbox } from "../Checkbox";
67

78
type DummyData = {
89
id: number;
@@ -80,3 +81,95 @@ export const DefaultVariant: Story = {
8081
data: data
8182
}
8283
};
84+
85+
export const CustomizedVariant: Story = {
86+
name: "Customized",
87+
render: () => <CustomizedDataTable />,
88+
args: {
89+
// provide in component below
90+
columns: [],
91+
data: []
92+
}
93+
};
94+
95+
injectSortingArrowIcons({
96+
ArrowUp: () => <div></div>,
97+
ArrowDown: () => <div></div>
98+
});
99+
100+
const CustomizedDataTable = () => {
101+
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
102+
const [sorting, setSorting] = useState<SortingState>([
103+
{
104+
id: "name",
105+
desc: true
106+
},
107+
{
108+
id: "age",
109+
desc: false
110+
}
111+
]);
112+
113+
return (
114+
<div>
115+
<DataTable
116+
columns={colsCustomized}
117+
data={data}
118+
getRowId={(x) => x.id.toString()}
119+
rowSelection={rowSelection}
120+
onRowSelectionChange={setRowSelection}
121+
sorting={sorting}
122+
onSortingChange={setSorting}
123+
/>
124+
125+
<div className="mt-4">
126+
<p>Selected rows:</p>
127+
<div className="flex gap-2">
128+
{Object.keys(rowSelection).map((key) => (
129+
<span key={key}>{key}</span>
130+
))}
131+
</div>
132+
</div>
133+
</div>
134+
);
135+
};
136+
137+
const colsCustomized: ColumnDef<DummyData, unknown>[] = [
138+
{
139+
id: "select",
140+
accessorKey: "select",
141+
header: ({ table }) => (
142+
<Checkbox
143+
id="select-all"
144+
checked={table.getIsAllRowsSelected()}
145+
onCheckedChange={(state) =>
146+
table.toggleAllRowsSelected(state === "indeterminate" ? true : state)
147+
}
148+
/>
149+
),
150+
cell: ({ row }) => (
151+
<Checkbox
152+
id={`select-${row.id}`}
153+
checked={row.getIsSelected()}
154+
onCheckedChange={row.getToggleSelectedHandler()}
155+
/>
156+
)
157+
},
158+
{
159+
id: "id",
160+
header: "ID",
161+
accessorKey: "id"
162+
},
163+
{
164+
id: "name",
165+
header: "Name",
166+
accessorKey: "name",
167+
enableSorting: true
168+
},
169+
{
170+
id: "age",
171+
header: "Age",
172+
accessorKey: "age",
173+
enableSorting: true
174+
}
175+
];

src/components/Table/DataTable.tsx

+109-14
Original file line numberDiff line numberDiff line change
@@ -1,23 +1,77 @@
11
"use client";
22

3-
import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
4-
import React, { useState } from "react";
3+
import React from "react";
4+
5+
import {
6+
ColumnDef,
7+
CoreOptions,
8+
ExpandedState,
9+
flexRender,
10+
getCoreRowModel,
11+
getExpandedRowModel,
12+
OnChangeFn,
13+
RowSelectionState,
14+
SortingState,
15+
useReactTable
16+
} from "@tanstack/react-table";
17+
518
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./Table";
19+
import { SortableHeader } from "./Sorting";
20+
import { cn } from "../..";
621

722
interface DataTableProps<TData, TValue> {
823
columns: ColumnDef<TData, TValue>[];
924
data: TData[];
25+
filters?: Record<string, string>;
26+
resetFilters?: () => void;
27+
getRowId?: CoreOptions<TData>["getRowId"];
28+
rowSelection?: RowSelectionState;
29+
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
30+
sorting?: SortingState;
31+
onSortingChange?: OnChangeFn<SortingState>;
32+
expanded?: ExpandedState;
33+
onExpandedChange?: OnChangeFn<ExpandedState>;
34+
getSubRows?: (row: TData) => TData[];
1035
}
1136

12-
export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
13-
const [rowSelection, setRowSelection] = useState({});
37+
export function DataTable<TData, TValue>({
38+
columns,
39+
data,
40+
filters,
41+
resetFilters,
42+
sorting,
43+
onSortingChange,
44+
expanded,
45+
onExpandedChange,
46+
getSubRows,
47+
getRowId,
48+
rowSelection = {},
49+
onRowSelectionChange
50+
}: DataTableProps<TData, TValue>) {
1451
const table = useReactTable({
1552
data,
16-
columns,
17-
onRowSelectionChange: setRowSelection,
53+
columns: columns.map((col) => {
54+
// if col.enableSorting is not defined, set it to false
55+
if (col.enableSorting === undefined) {
56+
col.enableSorting = false;
57+
}
58+
return col;
59+
}),
60+
manualSorting: true,
61+
onRowSelectionChange,
62+
onSortingChange,
63+
enableSortingRemoval: false,
64+
enableMultiSort: false,
65+
enableRowSelection: true,
66+
onExpandedChange,
67+
getRowId,
68+
getSubRows,
1869
getCoreRowModel: getCoreRowModel(),
70+
getExpandedRowModel: getExpandedRowModel(),
1971
state: {
20-
rowSelection
72+
rowSelection,
73+
sorting,
74+
expanded
2175
}
2276
});
2377

@@ -28,16 +82,29 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
2882
{table.getHeaderGroups().map((headerGroup) => (
2983
<TableRow key={headerGroup.id}>
3084
{headerGroup.headers.map((header) => {
85+
const canSort = header.column.getCanSort();
3186
return (
3287
<TableHead
33-
className="text-theme-text-secondary"
88+
className={cn(
89+
header.column.columnDef.meta?.className,
90+
`${
91+
canSort ? "p-0" : ""
92+
} bg-theme-surface-tertiary font-semibold text-theme-text-secondary`
93+
)}
3494
key={header.id}
35-
// @ts-expect-error width doesnt exist on the type, and would need a global fragmentation
36-
style={{ width: header.column.columnDef.meta?.width || undefined }}
95+
style={{
96+
width: header.column.columnDef.meta?.width
97+
}}
3798
>
38-
{header.isPlaceholder
39-
? null
40-
: flexRender(header.column.columnDef.header, header.getContext())}
99+
{canSort ? (
100+
<SortableHeader header={header}>
101+
{header.isPlaceholder
102+
? null
103+
: flexRender(header.column.columnDef.header, header.getContext())}
104+
</SortableHeader>
105+
) : header.isPlaceholder ? null : (
106+
flexRender(header.column.columnDef.header, header.getContext())
107+
)}
41108
</TableHead>
42109
);
43110
})}
@@ -53,12 +120,40 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
53120
data-state={row.getIsSelected() && "selected"}
54121
>
55122
{row.getVisibleCells().map((cell) => (
56-
<TableCell className="font-medium text-theme-text-primary" key={cell.id}>
123+
<TableCell
124+
className={cn(
125+
"font-medium text-theme-text-primary",
126+
cell.column.columnDef.meta?.className
127+
)}
128+
key={cell.id}
129+
>
57130
{flexRender(cell.column.columnDef.cell, cell.getContext())}
58131
</TableCell>
59132
))}
60133
</TableRow>
61134
))
135+
) : filters && Object.keys(filters).length ? (
136+
<TableRow>
137+
<TableCell
138+
colSpan={columns.length}
139+
className="h-24 bg-theme-surface-primary p-9 text-center"
140+
>
141+
<p className="text-text-lg text-theme-text-secondary">
142+
No items match your current selection.
143+
</p>
144+
<p className="text-text-lg text-theme-text-secondary">
145+
Refine your filters and try again.
146+
</p>
147+
<div className="mt-5">
148+
<p
149+
className="inline-block cursor-pointer text-text-lg text-theme-text-brand underline"
150+
onClick={resetFilters}
151+
>
152+
Clear filters
153+
</p>
154+
</div>
155+
</TableCell>
156+
</TableRow>
62157
) : (
63158
<TableRow>
64159
<TableCell
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
1+
import React, { useState, createContext, ReactNode, SetStateAction, Dispatch } from "react";
2+
import { RowSelectionState } from "@tanstack/react-table";
3+
4+
export function createDataTableConsumerContext<CtxProviderProps extends { children: ReactNode }>() {
5+
type CtxValue = {
6+
rowSelection: RowSelectionState;
7+
setRowSelection: Dispatch<SetStateAction<RowSelectionState>>;
8+
9+
selectedRowIDs: string[];
10+
selectedRowCount: number;
11+
};
12+
13+
const Context = createContext<CtxValue | null>(null);
14+
15+
function useContext() {
16+
const ctx = React.useContext(Context);
17+
if (!ctx) throw new Error("DataTableConsumerContext must be used within its Provider");
18+
return ctx;
19+
}
20+
21+
function ContextProvider({ children }: CtxProviderProps) {
22+
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
23+
24+
const selectedRowIDs = getSelectedRowIDs(rowSelection);
25+
const selectedRowCount = selectedRowIDs.length;
26+
27+
return (
28+
<Context.Provider value={{ rowSelection, setRowSelection, selectedRowIDs, selectedRowCount }}>
29+
{children}
30+
</Context.Provider>
31+
);
32+
}
33+
34+
return {
35+
Context,
36+
ContextProvider,
37+
useContext
38+
};
39+
}
40+
41+
export function countSelectedRows(rowSelection: RowSelectionState): number {
42+
return Object.values(rowSelection).reduce((acc, curr) => acc + Number(curr), 0);
43+
}
44+
45+
export function getSelectedRowIDs(rowSelection: RowSelectionState): string[] {
46+
return Object.entries(rowSelection)
47+
.filter(([, selected]) => selected)
48+
.map(([id]) => id);
49+
}

src/components/Table/Icons.tsx

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
import React from "react";
2+
3+
export type SortingArrowIcon = React.FC<{
4+
className?: string;
5+
}>;
6+
7+
export let ArrowUp: SortingArrowIcon = () => <></>;
8+
export let ArrowDown: SortingArrowIcon = () => <></>;
9+
10+
export function injectSortingArrowIcons(icons: {
11+
ArrowUp: SortingArrowIcon;
12+
ArrowDown: SortingArrowIcon;
13+
}) {
14+
ArrowUp = icons.ArrowUp;
15+
ArrowDown = icons.ArrowDown;
16+
}

src/components/Table/Sorting.tsx

+37
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
import React from "react";
2+
import { Header, RowData, SortDirection } from "@tanstack/react-table";
3+
import { PropsWithChildren } from "react";
4+
import { ArrowUp, ArrowDown } from "./Icons";
5+
6+
type Props = {
7+
direction: SortDirection | false;
8+
};
9+
10+
function SortingArrow({ direction }: Props) {
11+
if (direction === false) return null;
12+
13+
const Comp = direction === "asc" ? ArrowUp : ArrowDown;
14+
15+
return <Comp className="h-4 w-4 shrink-0" />;
16+
}
17+
18+
interface HeaderProps<TData extends RowData> {
19+
header: Header<TData, unknown>;
20+
}
21+
22+
export function SortableHeader<TData extends RowData>({
23+
header,
24+
children
25+
}: PropsWithChildren<HeaderProps<TData>>) {
26+
return (
27+
<button
28+
className={`${
29+
header.column.getIsSorted() ? "text-theme-text-primary" : ""
30+
} flex h-full w-full items-center gap-1 px-4 py-2 text-text-sm transition-all duration-100 hover:bg-gray-200`}
31+
onClick={header.column.getToggleSortingHandler()}
32+
>
33+
<span>{children}</span>
34+
<SortingArrow direction={header.column.getIsSorted()} />
35+
</button>
36+
);
37+
}

src/components/Table/index.tsx

+3
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,5 @@
11
export * from "./Table";
22
export * from "./DataTable";
3+
export * from "./DataTableConsumerContext";
4+
export * from "./Sorting";
5+
export * from "./Icons";

src/components/client.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
export * from "./Sidebar";
33
export * from "./Avatar";
44
export * from "./Dropdown";
5-
export * from "./Table/DataTable";
5+
export * from "./Table";
66
export * from "./Sheet";
77
export * from "./Tabs";
88
export * from "./Collapsible";

0 commit comments

Comments
 (0)