From 875686364a677c442d7f1e013b2fa57a2a462ce6 Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Thu, 6 Mar 2025 10:06:15 +0200 Subject: [PATCH 1/4] port DataTable functionality from Pro --- src/components/Table/DataTable.tsx | 123 ++++++++++++++++-- .../Table/DataTableConsumerContext.tsx | 49 +++++++ src/components/Table/Sorting.tsx | 41 ++++++ src/components/Table/index.tsx | 2 + src/components/client.ts | 2 +- src/table.d.ts | 8 ++ 6 files changed, 210 insertions(+), 15 deletions(-) create mode 100644 src/components/Table/DataTableConsumerContext.tsx create mode 100644 src/components/Table/Sorting.tsx create mode 100644 src/table.d.ts diff --git a/src/components/Table/DataTable.tsx b/src/components/Table/DataTable.tsx index a2fd6d0..37cfd5e 100644 --- a/src/components/Table/DataTable.tsx +++ b/src/components/Table/DataTable.tsx @@ -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 { columns: ColumnDef[]; data: TData[]; + filters?: Record; + resetFilters?: () => void; + getRowId?: CoreOptions["getRowId"]; + rowSelection?: RowSelectionState; + onRowSelectionChange?: OnChangeFn; + sorting?: SortingState; + onSortingChange?: OnChangeFn; + expanded?: ExpandedState; + onExpandedChange?: OnChangeFn; + getSubRows?: (row: TData) => TData[]; } -export function DataTable({ columns, data }: DataTableProps) { - const [rowSelection, setRowSelection] = useState({}); +export function DataTable({ + columns, + data, + filters, + resetFilters, + sorting, + onSortingChange, + expanded, + onExpandedChange, + getSubRows, + getRowId, + rowSelection = {}, + onRowSelectionChange +}: DataTableProps) { 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 } }); @@ -28,16 +82,29 @@ export function DataTable({ columns, data }: DataTableProps ( {headerGroup.headers.map((header) => { + const canSort = header.column.getCanSort(); return ( - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} + {canSort ? ( + + {header.isPlaceholder + ? null + : flexRender(header.column.columnDef.header, header.getContext())} + + ) : header.isPlaceholder ? null : ( + flexRender(header.column.columnDef.header, header.getContext()) + )} ); })} @@ -53,12 +120,40 @@ export function DataTable({ columns, data }: DataTableProps {row.getVisibleCells().map((cell) => ( - + {flexRender(cell.column.columnDef.cell, cell.getContext())} ))} )) + ) : filters && Object.keys(filters).length ? ( + + +

+ No items match your current selection. +

+

+ Refine your filters and try again. +

+
+

+ Clear filters +

+
+
+
) : ( () { + type CtxValue = { + rowSelection: RowSelectionState; + setRowSelection: Dispatch>; + + selectedRowIDs: string[]; + selectedRowCount: number; + }; + + const Context = createContext(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({}); + + const selectedRowIDs = getSelectedRowIDs(rowSelection); + const selectedRowCount = selectedRowIDs.length; + + return ( + + {children} + + ); + } + + 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); +} diff --git a/src/components/Table/Sorting.tsx b/src/components/Table/Sorting.tsx new file mode 100644 index 0000000..7958349 --- /dev/null +++ b/src/components/Table/Sorting.tsx @@ -0,0 +1,41 @@ +import React from "react"; +import { Header, RowData /*, SortDirection*/ } from "@tanstack/react-table"; +// TODO FIXME: importing directly not allowed? +// import ArrowUp from "../../assets/icons/arrow-up.svg"; +// import ArrowDown from "../../assets/icons/arrow-down.svg"; +import { PropsWithChildren } from "react"; + +/* +type Props = { + direction: SortDirection | false; +}; + +function SortingArrow({ direction }: Props) { + if (direction === false) return null; + + const Comp = direction === "asc" ? ArrowUp : ArrowDown; + + return ; +} +*/ + +interface HeaderProps { + header: Header; +} + +export function SortableHeader({ + header, + children +}: PropsWithChildren>) { + return ( + + ); +} diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 3d55021..2e6ca49 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -1,2 +1,4 @@ export * from "./Table"; export * from "./DataTable"; +export * from "./DataTableConsumerContext"; +export * from "./Sorting"; diff --git a/src/components/client.ts b/src/components/client.ts index de745b0..b72890e 100644 --- a/src/components/client.ts +++ b/src/components/client.ts @@ -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"; diff --git a/src/table.d.ts b/src/table.d.ts new file mode 100644 index 0000000..06967fa --- /dev/null +++ b/src/table.d.ts @@ -0,0 +1,8 @@ +import "@tanstack/react-table"; //or vue, svelte, solid, qwik, etc. + +declare module "@tanstack/react-table" { + interface ColumnMeta { + width?: string; + className?: string; + } +} From 78c15d3f3caaa6005185e983fe200b2f94b0b46a Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Tue, 11 Mar 2025 08:31:45 +0200 Subject: [PATCH 2/4] implement customized DataTable story --- src/components/Table/DataTable.stories.tsx | 78 +++++++++++++++++++++- 1 file changed, 76 insertions(+), 2 deletions(-) diff --git a/src/components/Table/DataTable.stories.tsx b/src/components/Table/DataTable.stories.tsx index 399cbb0..b191504 100644 --- a/src/components/Table/DataTable.stories.tsx +++ b/src/components/Table/DataTable.stories.tsx @@ -1,8 +1,9 @@ import { Meta } from "@storybook/react"; import { StoryObj } from "@storybook/react"; -import React from "react"; +import React, { useState } from "react"; import { DataTable } from "./index"; -import { ColumnDef } from "@tanstack/react-table"; +import { ColumnDef, RowSelectionState } from "@tanstack/react-table"; +import { Checkbox } from "../Checkbox"; type DummyData = { id: number; @@ -80,3 +81,76 @@ export const DefaultVariant: Story = { data: data } }; + +export const CustomizedVariant: Story = { + name: "Customized", + render: () => , + args: { + // provide in component below + columns: [], + data: [] + } +}; + +const CustomizedDataTable = () => { + const [rowSelection, setRowSelection] = useState({}); + + return ( +
+ x.id.toString()} + rowSelection={rowSelection} + onRowSelectionChange={setRowSelection} + /> + +
+

Selected rows:

+
+ {Object.keys(rowSelection).map((key) => ( + {key} + ))} +
+
+
+ ); +}; + +const colsCustomized: ColumnDef[] = [ + { + id: "select", + accessorKey: "select", + header: ({ table }) => ( + + table.toggleAllRowsSelected(state === "indeterminate" ? true : state) + } + /> + ), + cell: ({ row }) => ( + + ) + }, + { + id: "id", + header: "ID", + accessorKey: "id" + }, + { + id: "name", + header: "Name", + accessorKey: "name" + }, + { + id: "age", + header: "Age", + accessorKey: "age" + } +]; From 1694673d13caf08b790f48049673f9ef2cf8f728 Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Tue, 11 Mar 2025 08:39:47 +0200 Subject: [PATCH 3/4] create mechanism to inject icons --- src/components/Table/Icons.tsx | 16 ++++++++++++++++ src/components/Table/Sorting.tsx | 10 +++------- src/components/Table/index.tsx | 1 + 3 files changed, 20 insertions(+), 7 deletions(-) create mode 100644 src/components/Table/Icons.tsx diff --git a/src/components/Table/Icons.tsx b/src/components/Table/Icons.tsx new file mode 100644 index 0000000..3645734 --- /dev/null +++ b/src/components/Table/Icons.tsx @@ -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; +} diff --git a/src/components/Table/Sorting.tsx b/src/components/Table/Sorting.tsx index 7958349..a6212bb 100644 --- a/src/components/Table/Sorting.tsx +++ b/src/components/Table/Sorting.tsx @@ -1,11 +1,8 @@ import React from "react"; -import { Header, RowData /*, SortDirection*/ } from "@tanstack/react-table"; -// TODO FIXME: importing directly not allowed? -// import ArrowUp from "../../assets/icons/arrow-up.svg"; -// import ArrowDown from "../../assets/icons/arrow-down.svg"; +import { Header, RowData, SortDirection } from "@tanstack/react-table"; import { PropsWithChildren } from "react"; +import { ArrowUp, ArrowDown } from "./Icons"; -/* type Props = { direction: SortDirection | false; }; @@ -17,7 +14,6 @@ function SortingArrow({ direction }: Props) { return ; } -*/ interface HeaderProps { header: Header; @@ -35,7 +31,7 @@ export function SortableHeader({ onClick={header.column.getToggleSortingHandler()} > {children} - {/* */} + ); } diff --git a/src/components/Table/index.tsx b/src/components/Table/index.tsx index 2e6ca49..3941524 100644 --- a/src/components/Table/index.tsx +++ b/src/components/Table/index.tsx @@ -2,3 +2,4 @@ export * from "./Table"; export * from "./DataTable"; export * from "./DataTableConsumerContext"; export * from "./Sorting"; +export * from "./Icons"; From 2faf23b3551de13eddbd10d6cb334cd4ab6f677d Mon Sep 17 00:00:00 2001 From: Kipras Melnikovas Date: Tue, 11 Mar 2025 08:55:41 +0200 Subject: [PATCH 4/4] showcase sorting & custom icon injection --- .changeset/yummy-ends-knock.md | 5 ++++ src/components/Table/DataTable.stories.tsx | 27 ++++++++++++++++++---- 2 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 .changeset/yummy-ends-knock.md diff --git a/.changeset/yummy-ends-knock.md b/.changeset/yummy-ends-knock.md new file mode 100644 index 0000000..b677dc2 --- /dev/null +++ b/.changeset/yummy-ends-knock.md @@ -0,0 +1,5 @@ +--- +"@zenml-io/react-component-library": minor +--- + +port full functionality of DataTable component from pro diff --git a/src/components/Table/DataTable.stories.tsx b/src/components/Table/DataTable.stories.tsx index b191504..a9eed33 100644 --- a/src/components/Table/DataTable.stories.tsx +++ b/src/components/Table/DataTable.stories.tsx @@ -1,8 +1,8 @@ import { Meta } from "@storybook/react"; import { StoryObj } from "@storybook/react"; import React, { useState } from "react"; -import { DataTable } from "./index"; -import { ColumnDef, RowSelectionState } from "@tanstack/react-table"; +import { DataTable, injectSortingArrowIcons } from "./index"; +import { ColumnDef, RowSelectionState, SortingState } from "@tanstack/react-table"; import { Checkbox } from "../Checkbox"; type DummyData = { @@ -92,8 +92,23 @@ export const CustomizedVariant: Story = { } }; +injectSortingArrowIcons({ + ArrowUp: () =>
, + ArrowDown: () =>
+}); + const CustomizedDataTable = () => { const [rowSelection, setRowSelection] = useState({}); + const [sorting, setSorting] = useState([ + { + id: "name", + desc: true + }, + { + id: "age", + desc: false + } + ]); return (
@@ -103,6 +118,8 @@ const CustomizedDataTable = () => { getRowId={(x) => x.id.toString()} rowSelection={rowSelection} onRowSelectionChange={setRowSelection} + sorting={sorting} + onSortingChange={setSorting} />
@@ -146,11 +163,13 @@ const colsCustomized: ColumnDef[] = [ { id: "name", header: "Name", - accessorKey: "name" + accessorKey: "name", + enableSorting: true }, { id: "age", header: "Age", - accessorKey: "age" + accessorKey: "age", + enableSorting: true } ];