Skip to content

port DataTable functionality from Pro #102

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Mar 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/yummy-ends-knock.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@zenml-io/react-component-library": minor
---

port full functionality of DataTable component from pro
99 changes: 96 additions & 3 deletions src/components/Table/DataTable.stories.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,9 @@
import { Meta } from "@storybook/react";
import { StoryObj } from "@storybook/react";
import React from "react";
import { DataTable } from "./index";
import { ColumnDef } from "@tanstack/react-table";
import React, { useState } from "react";
import { DataTable, injectSortingArrowIcons } from "./index";
import { ColumnDef, RowSelectionState, SortingState } from "@tanstack/react-table";
import { Checkbox } from "../Checkbox";

type DummyData = {
id: number;
Expand Down Expand Up @@ -80,3 +81,95 @@ export const DefaultVariant: Story = {
data: data
}
};

export const CustomizedVariant: Story = {
name: "Customized",
render: () => <CustomizedDataTable />,
args: {
// provide in component below
columns: [],
data: []
}
};

injectSortingArrowIcons({
ArrowUp: () => <div>↑</div>,
ArrowDown: () => <div>↓</div>
});

const CustomizedDataTable = () => {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});
const [sorting, setSorting] = useState<SortingState>([
{
id: "name",
desc: true
},
{
id: "age",
desc: false
}
]);

return (
<div>
<DataTable
columns={colsCustomized}
data={data}
getRowId={(x) => x.id.toString()}
rowSelection={rowSelection}
onRowSelectionChange={setRowSelection}
sorting={sorting}
onSortingChange={setSorting}
Comment on lines +121 to +122
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

i don't think sorting here actually works. any idea what's missing?

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is also a useSorting hook involved, that is missing here I think 🤔

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

but that hook in pro is only for syncing the sorting state to the router/url. i don't think it's needed here because it's specific for our use case. or did i miss something and it has logic that makes the sorting work?

i checked it again, i see that the state gets set in the url and then taken in & applied. but can't we have the sorting work w/ local state first, instead of putting it in the url? at least for the example here

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hm, I'm not sure tbh. The difference might be that in Pro were doing server-side sorting, while here you want to rely on client-only sorting https://tanstack.com/table/v8/docs/guide/sorting#client-side-vs-server-side-sorting

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

hmm that's tough. because in the storybook demo it's not working at all. i'll take a look at how to fix this, but in the meantime i think we can merge, since it'll work fine in our repos?

/>

<div className="mt-4">
<p>Selected rows:</p>
<div className="flex gap-2">
{Object.keys(rowSelection).map((key) => (
<span key={key}>{key}</span>
))}
</div>
</div>
</div>
);
};

const colsCustomized: ColumnDef<DummyData, unknown>[] = [
{
id: "select",
accessorKey: "select",
header: ({ table }) => (
<Checkbox
id="select-all"
checked={table.getIsAllRowsSelected()}
onCheckedChange={(state) =>
table.toggleAllRowsSelected(state === "indeterminate" ? true : state)
}
/>
),
cell: ({ row }) => (
<Checkbox
id={`select-${row.id}`}
checked={row.getIsSelected()}
onCheckedChange={row.getToggleSelectedHandler()}
/>
)
},
{
id: "id",
header: "ID",
accessorKey: "id"
},
{
id: "name",
header: "Name",
accessorKey: "name",
enableSorting: true
},
{
id: "age",
header: "Age",
accessorKey: "age",
enableSorting: true
}
];
123 changes: 109 additions & 14 deletions src/components/Table/DataTable.tsx
Original file line number Diff line number Diff line change
@@ -1,23 +1,77 @@
"use client";

import { ColumnDef, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table";
import React, { useState } from "react";
import React from "react";

import {
ColumnDef,
CoreOptions,
ExpandedState,
flexRender,
getCoreRowModel,
getExpandedRowModel,
OnChangeFn,
RowSelectionState,
SortingState,
useReactTable
} from "@tanstack/react-table";

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./Table";
import { SortableHeader } from "./Sorting";
import { cn } from "../..";

interface DataTableProps<TData, TValue> {
columns: ColumnDef<TData, TValue>[];
data: TData[];
filters?: Record<string, string>;
resetFilters?: () => void;
getRowId?: CoreOptions<TData>["getRowId"];
rowSelection?: RowSelectionState;
onRowSelectionChange?: OnChangeFn<RowSelectionState>;
sorting?: SortingState;
onSortingChange?: OnChangeFn<SortingState>;
expanded?: ExpandedState;
onExpandedChange?: OnChangeFn<ExpandedState>;
getSubRows?: (row: TData) => TData[];
}

export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData, TValue>) {
const [rowSelection, setRowSelection] = useState({});
export function DataTable<TData, TValue>({
columns,
data,
filters,
resetFilters,
sorting,
onSortingChange,
expanded,
onExpandedChange,
getSubRows,
getRowId,
rowSelection = {},
onRowSelectionChange
}: DataTableProps<TData, TValue>) {
const table = useReactTable({
data,
columns,
onRowSelectionChange: setRowSelection,
columns: columns.map((col) => {
// if col.enableSorting is not defined, set it to false
if (col.enableSorting === undefined) {
col.enableSorting = false;
}
return col;
}),
manualSorting: true,
onRowSelectionChange,
onSortingChange,
enableSortingRemoval: false,
enableMultiSort: false,
enableRowSelection: true,
onExpandedChange,
getRowId,
getSubRows,
getCoreRowModel: getCoreRowModel(),
getExpandedRowModel: getExpandedRowModel(),
state: {
rowSelection
rowSelection,
sorting,
expanded
}
});

Expand All @@ -28,16 +82,29 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id}>
{headerGroup.headers.map((header) => {
const canSort = header.column.getCanSort();
return (
<TableHead
className="text-theme-text-secondary"
className={cn(
header.column.columnDef.meta?.className,
`${
canSort ? "p-0" : ""
} bg-theme-surface-tertiary font-semibold text-theme-text-secondary`
)}
key={header.id}
// @ts-expect-error width doesnt exist on the type, and would need a global fragmentation
style={{ width: header.column.columnDef.meta?.width || undefined }}
style={{
width: header.column.columnDef.meta?.width
}}
>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{canSort ? (
<SortableHeader header={header}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
</SortableHeader>
) : header.isPlaceholder ? null : (
flexRender(header.column.columnDef.header, header.getContext())
)}
</TableHead>
);
})}
Expand All @@ -53,12 +120,40 @@ export function DataTable<TData, TValue>({ columns, data }: DataTableProps<TData
data-state={row.getIsSelected() && "selected"}
>
{row.getVisibleCells().map((cell) => (
<TableCell className="font-medium text-theme-text-primary" key={cell.id}>
<TableCell
className={cn(
"font-medium text-theme-text-primary",
cell.column.columnDef.meta?.className
)}
key={cell.id}
>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
))}
</TableRow>
))
) : filters && Object.keys(filters).length ? (
<TableRow>
<TableCell
colSpan={columns.length}
className="h-24 bg-theme-surface-primary p-9 text-center"
>
<p className="text-text-lg text-theme-text-secondary">
No items match your current selection.
</p>
<p className="text-text-lg text-theme-text-secondary">
Refine your filters and try again.
</p>
<div className="mt-5">
<p
className="inline-block cursor-pointer text-text-lg text-theme-text-brand underline"
onClick={resetFilters}
>
Clear filters
</p>
</div>
</TableCell>
</TableRow>
) : (
<TableRow>
<TableCell
Expand Down
49 changes: 49 additions & 0 deletions src/components/Table/DataTableConsumerContext.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
import React, { useState, createContext, ReactNode, SetStateAction, Dispatch } from "react";
import { RowSelectionState } from "@tanstack/react-table";

export function createDataTableConsumerContext<CtxProviderProps extends { children: ReactNode }>() {
type CtxValue = {
rowSelection: RowSelectionState;
setRowSelection: Dispatch<SetStateAction<RowSelectionState>>;

selectedRowIDs: string[];
selectedRowCount: number;
};

const Context = createContext<CtxValue | null>(null);

function useContext() {
const ctx = React.useContext(Context);
if (!ctx) throw new Error("DataTableConsumerContext must be used within its Provider");
return ctx;
}

function ContextProvider({ children }: CtxProviderProps) {
const [rowSelection, setRowSelection] = useState<RowSelectionState>({});

const selectedRowIDs = getSelectedRowIDs(rowSelection);
const selectedRowCount = selectedRowIDs.length;

return (
<Context.Provider value={{ rowSelection, setRowSelection, selectedRowIDs, selectedRowCount }}>
{children}
</Context.Provider>
);
}

return {
Context,
ContextProvider,
useContext
};
}

export function countSelectedRows(rowSelection: RowSelectionState): number {
return Object.values(rowSelection).reduce((acc, curr) => acc + Number(curr), 0);
}

export function getSelectedRowIDs(rowSelection: RowSelectionState): string[] {
return Object.entries(rowSelection)
.filter(([, selected]) => selected)
.map(([id]) => id);
}
16 changes: 16 additions & 0 deletions src/components/Table/Icons.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import React from "react";

export type SortingArrowIcon = React.FC<{
className?: string;
}>;

export let ArrowUp: SortingArrowIcon = () => <></>;
export let ArrowDown: SortingArrowIcon = () => <></>;

export function injectSortingArrowIcons(icons: {
ArrowUp: SortingArrowIcon;
ArrowDown: SortingArrowIcon;
}) {
ArrowUp = icons.ArrowUp;
ArrowDown = icons.ArrowDown;
}
37 changes: 37 additions & 0 deletions src/components/Table/Sorting.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import React from "react";
import { Header, RowData, SortDirection } from "@tanstack/react-table";
import { PropsWithChildren } from "react";
import { ArrowUp, ArrowDown } from "./Icons";

type Props = {
direction: SortDirection | false;
};

function SortingArrow({ direction }: Props) {
if (direction === false) return null;

const Comp = direction === "asc" ? ArrowUp : ArrowDown;

return <Comp className="h-4 w-4 shrink-0" />;
}

interface HeaderProps<TData extends RowData> {
header: Header<TData, unknown>;
}

export function SortableHeader<TData extends RowData>({
header,
children
}: PropsWithChildren<HeaderProps<TData>>) {
return (
<button
className={`${
header.column.getIsSorted() ? "text-theme-text-primary" : ""
} flex h-full w-full items-center gap-1 px-4 py-2 text-text-sm transition-all duration-100 hover:bg-gray-200`}
onClick={header.column.getToggleSortingHandler()}
>
<span>{children}</span>
<SortingArrow direction={header.column.getIsSorted()} />
</button>
);
}
3 changes: 3 additions & 0 deletions src/components/Table/index.tsx
Original file line number Diff line number Diff line change
@@ -1,2 +1,5 @@
export * from "./Table";
export * from "./DataTable";
export * from "./DataTableConsumerContext";
export * from "./Sorting";
export * from "./Icons";
2 changes: 1 addition & 1 deletion src/components/client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
export * from "./Sidebar";
export * from "./Avatar";
export * from "./Dropdown";
export * from "./Table/DataTable";
export * from "./Table";
export * from "./Sheet";
export * from "./Tabs";
export * from "./Collapsible";
Expand Down
Loading