>(
59 | ({ className, ...props }, ref) => (
60 | [role=checkbox]]:translate-y-[2px]", className)}
63 | {...props}
64 | />
65 | ),
66 | )
67 | TableCell.displayName = "TableCell"
68 |
69 | const TableCaption = React.forwardRef>(
70 | ({ className, ...props }, ref) => (
71 |
72 | ),
73 | )
74 | TableCaption.displayName = "TableCaption"
75 |
76 | export { Table, TableHeader, TableBody, TableFooter, TableHead, TableRow, TableCell, TableCaption }
77 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/filters/hooks/use-advanced-filter.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import type { Table } from "@tanstack/react-table"
4 | import { useSearchParams } from "next/navigation"
5 | import * as React from "react"
6 | import type { ColumnFilter, FilterType } from "../types"
7 | import { getColumnFilterTypesByValue } from "../utils"
8 |
9 | interface UseDialogsProps {
10 | table: Table
11 | }
12 |
13 | export function useDialogs({ table }: UseDialogsProps) {
14 | const [filters, setFilters] = React.useState([])
15 |
16 | const searchParams = useSearchParams()
17 |
18 | React.useEffect(() => {
19 | const urlFilters = JSON.parse(searchParams.get("filters") || "[]")
20 | setFilters(urlFilters)
21 | }, [searchParams])
22 |
23 | const handleChangeSearchParams = React.useCallback(
24 | (filters: ColumnFilter[]) => {
25 | const queryString = new URLSearchParams(searchParams)
26 | queryString.set("filters", JSON.stringify(filters))
27 | window.history.replaceState({}, "", `${window.location.pathname}?${queryString}`)
28 | },
29 | [searchParams],
30 | )
31 |
32 | const applyFilters = React.useCallback(() => {
33 | for (const filter of filters) {
34 | const column = table.getColumn(filter.id)
35 | if (column) {
36 | column.setFilterValue({ value: filter.value, type: filter.type })
37 | }
38 | }
39 | handleChangeSearchParams(filters)
40 | }, [filters, handleChangeSearchParams, table])
41 |
42 | const resetFilters = React.useCallback(() => {
43 | table.resetColumnFilters()
44 | setFilters([])
45 | handleChangeSearchParams([])
46 | }, [table, handleChangeSearchParams])
47 |
48 | const updateFilterValue = React.useCallback(
49 | (columnId: string, value: any) => {
50 | setFilters((prev) => {
51 | const existingFilter = prev.find((f) => f.id === columnId)
52 | if (existingFilter) return prev.map((f) => (f.id === columnId ? { ...f, value } : f))
53 |
54 | const sampleValue = table.getPrePaginationRowModel().rows?.[0]?.getValue(columnId)
55 | const availableFilterTypes = getColumnFilterTypesByValue(sampleValue)
56 | const defaultType = availableFilterTypes[0]
57 |
58 | return [...prev, { id: columnId, type: defaultType, value }]
59 | })
60 | },
61 | [table],
62 | )
63 |
64 | const updateFilterType = React.useCallback((columnId: string, type: FilterType) => {
65 | setFilters((prev) => {
66 | const existingFilter = prev.find((f) => f.id === columnId)
67 | if (existingFilter) {
68 | return prev.map((f) => (f.id === columnId ? { ...f, type, value: "" } : f))
69 | }
70 |
71 | return [...prev, { id: columnId, type, value: "" }]
72 | })
73 | }, [])
74 |
75 | const getCurrentFilter = React.useCallback(
76 | (columnId: string, defaultType: FilterType) => {
77 | const foundFilter = filters.find((f) => f.id === columnId)
78 | if (foundFilter) return foundFilter
79 | return { id: columnId, value: "", type: defaultType }
80 | },
81 | [filters],
82 | )
83 |
84 | return {
85 | filters,
86 | setFilters,
87 | applyFilters,
88 | resetFilters,
89 | updateFilterValue,
90 | updateFilterType,
91 | getCurrentFilter,
92 | }
93 | }
94 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/filters/utils.tsx:
--------------------------------------------------------------------------------
1 | import type { FilterFn, Row } from "@tanstack/react-table"
2 | import type { FilterType } from "./types"
3 |
4 | export const filterRows: FilterFn = (
5 | row: Row,
6 | columnId: string,
7 | filterValue: { value: any; type: FilterType },
8 | ): boolean => {
9 | const cellValue = row.getValue(columnId)
10 |
11 | switch (filterValue.type) {
12 | // String filters
13 | case "contains":
14 | return cellValue?.toString().toLowerCase().includes(filterValue.value?.toString().toLowerCase()) ?? true
15 | case "equals":
16 | return cellValue?.toString() === filterValue.value?.toString()
17 | case "startsWith":
18 | return cellValue?.toString().toLowerCase().startsWith(filterValue.value?.toString().toLowerCase()) ?? false
19 | case "endsWith":
20 | return cellValue?.toString().toLowerCase().endsWith(filterValue.value?.toString().toLowerCase()) ?? false
21 |
22 | // Number
23 | case "greaterThan":
24 | return Number(cellValue) > Number(filterValue.value)
25 | case "lessThan":
26 | return Number(cellValue) < Number(filterValue.value)
27 | case "greaterThanOrEqual":
28 | return Number(cellValue) >= Number(filterValue.value)
29 | case "lessThanOrEqual":
30 | return Number(cellValue) <= Number(filterValue.value)
31 | case "between": {
32 | const [min, max] = filterValue.value || []
33 | return Number(cellValue) >= Number(min) && Number(cellValue) <= Number(max)
34 | }
35 |
36 | // Date
37 | case "dateEquals":
38 | return new Date(cellValue as Date).getTime() === new Date(filterValue.value).getTime()
39 | case "dateBefore":
40 | return new Date(cellValue as Date).getTime() < new Date(filterValue.value).getTime()
41 | case "dateAfter":
42 | return new Date(cellValue as Date).getTime() > new Date(filterValue.value).getTime()
43 | case "dateBetween": {
44 | const [start, end] = filterValue.value || []
45 | const cellTime = new Date(cellValue as Date).getTime()
46 | return cellTime >= new Date(start).getTime() && cellTime <= new Date(end).getTime()
47 | }
48 |
49 | default:
50 | return true
51 | }
52 | }
53 |
54 | export type DetectedValueType = "number" | "date" | "string"
55 |
56 | export function detectValueType(value: unknown): DetectedValueType {
57 | if (value == null) return "string"
58 |
59 | if (typeof value === "number") {
60 | return "number"
61 | }
62 |
63 | if (typeof value === "string") {
64 | if (!Number.isNaN(Number(value)) && value.trim() !== "") {
65 | return "number"
66 | }
67 |
68 | if (value.includes("-") || value.includes("/")) {
69 | const parsed = new Date(value)
70 | if (!Number.isNaN(parsed.getTime())) {
71 | return "date"
72 | }
73 | }
74 | }
75 |
76 | if (value instanceof Date && !Number.isNaN(value.getTime())) {
77 | return "date"
78 | }
79 |
80 | return "string"
81 | }
82 |
83 | export function getColumnFilterTypesByValue(value: unknown, currentFilterType?: FilterType): FilterType[] {
84 | let type = detectValueType(value)
85 |
86 | if (value == null && currentFilterType) {
87 | type = getTypeFromFilterType(currentFilterType)
88 | }
89 |
90 | switch (type) {
91 | case "number":
92 | return ["equals", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual", "between"]
93 | case "date":
94 | return ["dateEquals", "dateBefore", "dateAfter", "dateBetween"]
95 | default:
96 | return ["contains", "equals", "startsWith", "endsWith"]
97 | }
98 | }
99 |
100 | function getTypeFromFilterType(filterType: FilterType): DetectedValueType {
101 | if (["between", "greaterThan", "lessThan", "greaterThanOrEqual", "lessThanOrEqual"].includes(filterType)) {
102 | return "number"
103 | }
104 |
105 | if (["dateBetween", "dateEquals", "dateBefore", "dateAfter"].includes(filterType)) {
106 | return "date"
107 | }
108 |
109 | return "string"
110 | }
111 |
--------------------------------------------------------------------------------
/src/components/ui/dialog.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DialogPrimitive from "@radix-ui/react-dialog"
5 | import { X } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Dialog = DialogPrimitive.Root
10 |
11 | const DialogTrigger = DialogPrimitive.Trigger
12 |
13 | const DialogPortal = DialogPrimitive.Portal
14 |
15 | const DialogClose = DialogPrimitive.Close
16 |
17 | const DialogOverlay = React.forwardRef<
18 | React.ElementRef,
19 | React.ComponentPropsWithoutRef
20 | >(({ className, ...props }, ref) => (
21 |
29 | ))
30 | DialogOverlay.displayName = DialogPrimitive.Overlay.displayName
31 |
32 | const DialogContent = React.forwardRef<
33 | React.ElementRef,
34 | React.ComponentPropsWithoutRef
35 | >(({ className, children, ...props }, ref) => (
36 |
37 |
38 |
46 | {children}
47 |
48 |
49 | Close
50 |
51 |
52 |
53 | ))
54 | DialogContent.displayName = DialogPrimitive.Content.displayName
55 |
56 | const DialogHeader = ({ className, ...props }: React.HTMLAttributes) => (
57 |
58 | )
59 | DialogHeader.displayName = "DialogHeader"
60 |
61 | const DialogFooter = ({ className, ...props }: React.HTMLAttributes) => (
62 |
63 | )
64 | DialogFooter.displayName = "DialogFooter"
65 |
66 | const DialogTitle = React.forwardRef<
67 | React.ElementRef,
68 | React.ComponentPropsWithoutRef
69 | >(({ className, ...props }, ref) => (
70 |
75 | ))
76 | DialogTitle.displayName = DialogPrimitive.Title.displayName
77 |
78 | const DialogDescription = React.forwardRef<
79 | React.ElementRef,
80 | React.ComponentPropsWithoutRef
81 | >(({ className, ...props }, ref) => (
82 |
83 | ))
84 | DialogDescription.displayName = DialogPrimitive.Description.displayName
85 |
86 | export {
87 | Dialog,
88 | DialogPortal,
89 | DialogOverlay,
90 | DialogTrigger,
91 | DialogClose,
92 | DialogContent,
93 | DialogHeader,
94 | DialogFooter,
95 | DialogTitle,
96 | DialogDescription,
97 | }
98 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/pagination.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { Input } from "@/components/ui/input"
3 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
4 | import { ChevronLeft, ChevronRight } from "lucide-react"
5 | import type React from "react"
6 | import { useState } from "react"
7 | import { useTableContext } from "../table-context"
8 |
9 | interface TablePaginationProps {
10 | options?: number[]
11 | enableGoToPage?: boolean
12 | }
13 |
14 | export function TablePagination({ options = [10, 20, 30, 40, 50], enableGoToPage = true }: TablePaginationProps) {
15 | const { table } = useTableContext()
16 | const [pageInput, setPageInput] = useState("")
17 |
18 | const handlePageInputChange = (e: React.ChangeEvent) => {
19 | setPageInput(e.target.value)
20 | }
21 |
22 | const handlePageInputKeyDown = (e: React.KeyboardEvent) => {
23 | if (e.key === "Enter") {
24 | const pageNumber = Number.parseInt(pageInput, 10)
25 | if (!Number.isNaN(pageNumber) && pageNumber > 0 && pageNumber <= table.getPageCount()) {
26 | table.setPageIndex(pageNumber - 1)
27 | setPageInput("")
28 | }
29 | }
30 | }
31 |
32 | return (
33 |
34 |
35 | {table.getFilteredSelectedRowModel().rows.length} of {table.getFilteredRowModel().rows.length} row(s) selected.
36 |
37 |
38 |
39 |
40 | Rows per page
41 |
59 |
60 |
61 | {enableGoToPage && (
62 |
63 |
64 | Page {table.getState().pagination.pageIndex + 1} of {table.getPageCount()}
65 |
66 |
67 |
77 |
78 | )}
79 |
80 |
81 |
90 |
91 |
100 |
101 |
102 |
103 | )
104 | }
105 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ## Overview
2 |
3 | This project showcases an enhanced table component built with React, `Shadcn UI`, and `@tanstack/react-table`. It offers a variety of interactive features, including column sorting, row reordering, data fetching, and filtering. The component is designed to be modular and highly customizable for modern web applications.
4 |
5 | ## Features
6 |
7 | - **Export Options**: PDF, CSV
8 | - **Filters**: Advanced and simple filter components (Dialog, Sheet)
9 | - **Reordering**: Column and row reordering (one at a time)
10 | - **Editing**
11 | - **Selection**
12 | - **Subrows**
13 | - **Hide Columns**
14 | - **Sorting**
15 |
16 | ## Preview (gif)
17 |
18 | *Insert preview GIF here*
19 |
20 | ### Full Featured Table
21 | - Expandable rows
22 | - Row selection
23 | - Column reordering
24 | - Editable cells
25 |
26 | ### Sortable Columns Table
27 | - Columns can be sorted
28 | - No row reordering
29 |
30 | ### Reorderable Rows Table
31 | - Rows can be reordered
32 | - No column sorting
33 |
34 | ## Installation
35 |
36 | To run this project locally, follow these steps:
37 |
38 | 1. **Clone the repository:**
39 | ```bash
40 | git clone https://github.com/your-repo/enhanced-table.git
41 | cd enhanced-table
42 | ```
43 | 2. **Install dependencies:**
44 | ```bash
45 | npm install
46 | # or
47 | yarn install
48 | ```
49 | 3. **Start the development server:**
50 | ```bash
51 | npm run dev
52 | # or
53 | yarn dev
54 | ```
55 |
56 | ## Usage
57 |
58 | Ensure that the API endpoint for fetching fake data (`http://localhost:3000/api/fake-data`) is available. Interact with the table using the provided UI controls:
59 |
60 | - **Set Data Count**: Adjust the input field to modify the number of records retrieved.
61 | - **Refresh Data**: Click the "Refresh Data" button to fetch new records.
62 | - **Switch Between Tabs**: Select different table modes using the tabbed interface.
63 |
64 | ## Use Example
65 |
66 | ```tsx
67 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 |
83 |
84 |
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 | ```
96 |
97 | ## Custom Row Styles Example
98 |
99 | Rows are styled based on their status:
100 |
101 | - **Active**: Green background
102 | - **Inactive**: Red background
103 | - **Pending**: Yellow background
104 |
105 | Example:
106 | ```tsx
107 | const customRowStyles = (row: Row) => {
108 | const baseStyles = "transition-colors hover:bg-opacity-20";
109 | const statusStyles = {
110 | active: "hover:bg-green-100 dark:hover:bg-green-900/50",
111 | inactive: "hover:bg-red-100 dark:hover:bg-red-900/50",
112 | pending: "hover:bg-yellow-100 dark:hover:bg-yellow-900/50",
113 | };
114 | return `${baseStyles} ${statusStyles[row.original.status]}`;
115 | };
116 | ```
117 |
118 | ## Code Structure
119 |
120 | - `src/app/(home)/_components/examples.tsx`: Contains the main table component with all functionalities.
121 | - `src/app/(home)/_components/columns.tsx`: Defines the table columns and associated configurations.
122 | - `@/components/enhanced-table`: Includes the core enhanced table component.
123 |
124 | ## Contributing
125 |
126 | Contributions are welcome! Follow these steps to contribute:
127 |
128 | 1. Fork the repository.
129 | 2. Create a new branch:
130 | ```bash
131 | git checkout -b feature-name
132 | ```
133 | 3. Commit your changes:
134 | ```bash
135 | git commit -m "Add new feature"
136 | ```
137 | 4. Push to the branch:
138 | ```bash
139 | git push origin feature-name
140 | ```
141 | 5. Create a Pull Request.
142 |
143 | ## License
144 |
145 | This project is licensed under the MIT License.
146 |
147 | ## Contact
148 |
149 | For any inquiries or support, reach out via [GitHub](https://github.com/drefahl).
150 |
--------------------------------------------------------------------------------
/src/components/ui/sheet.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SheetPrimitive from "@radix-ui/react-dialog"
5 | import { cva, type VariantProps } from "class-variance-authority"
6 | import { X } from "lucide-react"
7 |
8 | import { cn } from "@/lib/utils"
9 |
10 | const Sheet = SheetPrimitive.Root
11 |
12 | const SheetTrigger = SheetPrimitive.Trigger
13 |
14 | const SheetClose = SheetPrimitive.Close
15 |
16 | const SheetPortal = SheetPrimitive.Portal
17 |
18 | const SheetOverlay = React.forwardRef<
19 | React.ElementRef,
20 | React.ComponentPropsWithoutRef
21 | >(({ className, ...props }, ref) => (
22 |
30 | ))
31 | SheetOverlay.displayName = SheetPrimitive.Overlay.displayName
32 |
33 | const sheetVariants = cva(
34 | "fixed z-50 gap-4 bg-background p-6 shadow-lg transition ease-in-out data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:duration-300 data-[state=open]:duration-500",
35 | {
36 | variants: {
37 | side: {
38 | top: "inset-x-0 top-0 border-b data-[state=closed]:slide-out-to-top data-[state=open]:slide-in-from-top",
39 | bottom:
40 | "inset-x-0 bottom-0 border-t data-[state=closed]:slide-out-to-bottom data-[state=open]:slide-in-from-bottom",
41 | left: "inset-y-0 left-0 h-full w-3/4 border-r data-[state=closed]:slide-out-to-left data-[state=open]:slide-in-from-left sm:max-w-sm",
42 | right:
43 | "inset-y-0 right-0 h-full w-3/4 border-l data-[state=closed]:slide-out-to-right data-[state=open]:slide-in-from-right sm:max-w-sm",
44 | },
45 | },
46 | defaultVariants: {
47 | side: "right",
48 | },
49 | },
50 | )
51 |
52 | interface SheetContentProps
53 | extends React.ComponentPropsWithoutRef,
54 | VariantProps {}
55 |
56 | const SheetContent = React.forwardRef, SheetContentProps>(
57 | ({ side = "right", className, children, ...props }, ref) => (
58 |
59 |
60 |
61 | {children}
62 |
63 |
64 | Close
65 |
66 |
67 |
68 | ),
69 | )
70 | SheetContent.displayName = SheetPrimitive.Content.displayName
71 |
72 | const SheetHeader = ({ className, ...props }: React.HTMLAttributes) => (
73 |
74 | )
75 | SheetHeader.displayName = "SheetHeader"
76 |
77 | const SheetFooter = ({ className, ...props }: React.HTMLAttributes) => (
78 |
79 | )
80 | SheetFooter.displayName = "SheetFooter"
81 |
82 | const SheetTitle = React.forwardRef<
83 | React.ElementRef,
84 | React.ComponentPropsWithoutRef
85 | >(({ className, ...props }, ref) => (
86 |
87 | ))
88 | SheetTitle.displayName = SheetPrimitive.Title.displayName
89 |
90 | const SheetDescription = React.forwardRef<
91 | React.ElementRef,
92 | React.ComponentPropsWithoutRef
93 | >(({ className, ...props }, ref) => (
94 |
95 | ))
96 | SheetDescription.displayName = SheetPrimitive.Description.displayName
97 |
98 | export {
99 | Sheet,
100 | SheetPortal,
101 | SheetOverlay,
102 | SheetTrigger,
103 | SheetClose,
104 | SheetContent,
105 | SheetHeader,
106 | SheetFooter,
107 | SheetTitle,
108 | SheetDescription,
109 | }
110 |
--------------------------------------------------------------------------------
/src/app/(home)/_components/columns.tsx:
--------------------------------------------------------------------------------
1 | import { Badge } from "@/components/ui/badge"
2 | import type { ColumnDef } from "@tanstack/react-table"
3 | import { format } from "date-fns"
4 | import { Activity, Briefcase, Calendar, TrendingUp, User } from "lucide-react"
5 |
6 | export interface Person {
7 | id: number
8 | rstName: string
9 | lastName: string
10 | age: number
11 | visits: number
12 | status: "active" | "inactive" | "pending"
13 | progress: number
14 | department: "engineering" | "marketing" | "sales" | "design"
15 | createdAt: string
16 | avatar: string
17 | }
18 |
19 | export const columns: ColumnDef[] = [
20 | {
21 | id: "avatar",
22 | header: "",
23 | accessorKey: "avatar",
24 | cell: ({ row }) => (
25 |
26 | ),
27 | filterFn: "filterRows",
28 | meta: { export: { pdf: false } },
29 | },
30 | {
31 | id: "firstName",
32 | accessorKey: "firstName",
33 | header: () => (
34 |
35 |
36 | First Name
37 |
38 | ),
39 | meta: { align: "center", export: { pdf: { header: "First Name" } } },
40 | filterFn: "filterRows",
41 | },
42 | {
43 | id: "lastName",
44 | accessorKey: "lastName",
45 | header: "Last Name",
46 | meta: { align: "center" },
47 | filterFn: "filterRows",
48 | },
49 | {
50 | id: "age",
51 | accessorKey: "age",
52 | header: "Age",
53 | cell: ({ row }) => {row.getValue("age")},
54 | meta: { align: "center", export: { pdf: { header: "Age" } } },
55 | filterFn: "filterRows",
56 | },
57 | {
58 | id: "visits",
59 | accessorKey: "visits",
60 | header: () => (
61 |
62 |
63 | Visits
64 |
65 | ),
66 | meta: { align: "center", export: { pdf: { header: "Visits" } } },
67 | filterFn: "filterRows",
68 | },
69 | {
70 | id: "status",
71 | accessorKey: "status",
72 | header: "Status",
73 | cell: ({ row }) => (
74 |
75 | {row.getValue("status")}
76 |
77 | ),
78 | filterFn: "filterRows",
79 | meta: { export: { pdf: { header: "Status" } } },
80 | },
81 | {
82 | id: "progress",
83 | accessorKey: "progress",
84 | header: () => (
85 |
86 |
87 | Progress
88 |
89 | ),
90 | cell: ({ row }) => (
91 |
92 |
98 | {row.getValue("progress")}%
99 |
100 | ),
101 | filterFn: "filterRows",
102 | meta: { export: { pdf: { header: "Progress" } } },
103 | },
104 | {
105 | id: "department",
106 | accessorKey: "department",
107 | header: () => (
108 |
109 |
110 | Department
111 |
112 | ),
113 | cell: ({ row }) => {
114 | const department = row.getValue("department") as string
115 | const iconClass = "w-5 h-5 mr-2"
116 | const icons = {
117 | engineering: ,
118 | marketing: ,
119 | sales: ,
120 | design: ,
121 | }
122 | return (
123 |
124 | {icons[department as keyof typeof icons]}
125 | {department}
126 |
127 | )
128 | },
129 | filterFn: "filterRows",
130 | meta: { export: { pdf: { header: "Department" } } },
131 | },
132 | {
133 | id: "createdAt",
134 | accessorKey: "createdAt",
135 | header: () => (
136 |
137 |
138 | Created At
139 |
140 | ),
141 | accessorFn: (row) => format(new Date(row.createdAt), "MM/dd/yyyy"),
142 | cell: ({ row }) => {row.getValue("createdAt")},
143 | filterFn: "filterRows",
144 | meta: { export: { pdf: { header: "Created At" } } },
145 | },
146 | ]
147 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/toolbar/export-pdf.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import {
3 | DropdownMenu,
4 | DropdownMenuContent,
5 | DropdownMenuItem,
6 | DropdownMenuSeparator,
7 | DropdownMenuTrigger,
8 | } from "@/components/ui/dropdown-menu"
9 | import jsPDF from "jspdf"
10 | import autoTable from "jspdf-autotable"
11 | import { ChevronDown, Download, File, FileText } from "lucide-react"
12 | import { useTableContext } from "../../table-context"
13 | import { isSpecialId } from "../utils"
14 |
15 | export function ExportTable() {
16 | const { table } = useTableContext()
17 |
18 | const exportData = (format: "pdf" | "csv", dataType: "visible" | "all") => {
19 | const columns = table.getAllColumns().filter((column) => {
20 | if (isSpecialId(column.id)) return false
21 | const metaExport = column.columnDef.meta?.export
22 | if (metaExport === false) return false
23 | if (metaExport && typeof metaExport === "object") {
24 | if (format === "pdf" && metaExport.pdf === false) return false
25 | if (format === "csv" && metaExport.csv === false) return false
26 | }
27 | return true
28 | })
29 |
30 | const headers = columns.map((column) => {
31 | const metaExport = column.columnDef.meta?.export
32 | if (metaExport && typeof metaExport === "object") {
33 | if (format === "pdf" && metaExport.pdf && typeof metaExport.pdf === "object" && metaExport.pdf.header) {
34 | return metaExport.pdf.header
35 | }
36 | if (format === "csv" && metaExport.csv && typeof metaExport.csv === "object" && metaExport.csv.header) {
37 | return metaExport.csv.header
38 | }
39 | }
40 |
41 | const headerContent = column.columnDef.header
42 |
43 | if (typeof headerContent === "function") {
44 | return column.id || ""
45 | }
46 |
47 | return String(headerContent || column.id)
48 | })
49 |
50 | const rows = (dataType === "visible" ? table.getRowModel().rows : table.getCoreRowModel().rows).map((row) => {
51 | return columns.map((column) => {
52 | const value = row.getValue(column.id)
53 | return typeof value === "object" ? JSON.stringify(value) : String(value)
54 | })
55 | })
56 |
57 | if (format === "pdf") {
58 | exportToPDF(headers, rows)
59 | } else {
60 | exportToCSV(headers, rows)
61 | }
62 | }
63 |
64 | const exportToPDF = (headers: string[], rows: string[][]) => {
65 | try {
66 | const doc = new jsPDF()
67 |
68 | autoTable(doc, {
69 | head: [headers],
70 | body: rows,
71 | theme: "grid",
72 | styles: { fontSize: 8, cellPadding: 2 },
73 | headStyles: { fillColor: [41, 128, 185] },
74 | margin: { top: 20 },
75 | })
76 |
77 | doc.save("table_data.pdf")
78 | } catch (error) {
79 | console.error("Error generating PDF:", error)
80 | }
81 | }
82 |
83 | const exportToCSV = (headers: string[], rows: string[][]) => {
84 | try {
85 | const csvContent = [headers.join(","), ...rows.map((row) => row.join(","))].join("\n")
86 |
87 | const blob = new Blob([csvContent], { type: "text/csv;charset=utf-8;" })
88 | const link = document.createElement("a")
89 | if (link.download !== undefined) {
90 | const url = URL.createObjectURL(blob)
91 | link.setAttribute("href", url)
92 | link.setAttribute("download", "table_data.csv")
93 | link.style.visibility = "hidden"
94 | document.body.appendChild(link)
95 | link.click()
96 | document.body.removeChild(link)
97 | }
98 | } catch (error) {
99 | console.error("Error generating CSV:", error)
100 | }
101 | }
102 |
103 | return (
104 |
105 |
106 |
111 |
112 |
113 |
114 | exportData("pdf", "visible")}>
115 |
116 | Export Visible to PDF
117 |
118 |
119 | exportData("pdf", "all")}>
120 |
121 | Export All to PDF
122 |
123 |
124 |
125 |
126 | exportData("csv", "visible")}>
127 |
128 | Export Visible to CSV
129 |
130 |
131 | exportData("csv", "all")}>
132 |
133 | Export All to CSV
134 |
135 |
136 |
137 | )
138 | }
139 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/header/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { TableHead, TableRow, TableHeader as UiTableHeader } from "@/components/ui/table"
3 | import { cn } from "@/lib/utils"
4 | import { SortableContext, horizontalListSortingStrategy, useSortable } from "@dnd-kit/sortable"
5 | import { CSS } from "@dnd-kit/utilities"
6 | import { type Header, flexRender } from "@tanstack/react-table"
7 | import { ArrowUpDown, GripHorizontal } from "lucide-react"
8 | import type { CSSProperties } from "react"
9 | import { useTableContext } from "../../table-context"
10 | import { getAlignment, isSpecialId } from "../utils"
11 | import { HeaderDropdown } from "./dropdown"
12 |
13 | interface TableHeaderProps {
14 | variant?: "dropdown" | "default"
15 | }
16 |
17 | export function TableHeader({ variant = "default" }: TableHeaderProps) {
18 | const { table, enableColumnReorder, columnOrder } = useTableContext()
19 |
20 | return (
21 |
22 | {table.getHeaderGroups().map((headerGroup) => (
23 |
24 | {enableColumnReorder && columnOrder ? (
25 |
26 | {headerGroup.headers.map((header) => (
27 |
28 | ))}
29 |
30 | ) : (
31 | headerGroup.headers.map((header) => (
32 |
40 | {header.isPlaceholder ? null : (
41 |
42 | {header.column.getCanSort() ? (
43 | variant === "dropdown" ? (
44 |
45 | ) : (
46 |
54 | )
55 | ) : (
56 | flexRender(header.column.columnDef.header, header.getContext())
57 | )}
58 |
59 | )}
60 |
61 | ))
62 | )}
63 |
64 | ))}
65 |
66 | )
67 | }
68 |
69 | function DraggableTableHeader({
70 | header,
71 | variant,
72 | }: {
73 | header: Header
74 | variant?: "dropdown" | "default"
75 | }) {
76 | const { attributes, isDragging, listeners, setNodeRef, transform } = useSortable({
77 | id: header.column.id,
78 | })
79 |
80 | const style: CSSProperties = {
81 | opacity: isDragging ? 0.8 : 1,
82 | position: "relative",
83 | transform: CSS.Translate.toString(transform),
84 | transition: "width transform 0.2s ease-in-out",
85 | width: header.column.getSize(),
86 | zIndex: isDragging ? 1 : 0,
87 | whiteSpace: "nowrap",
88 | }
89 |
90 | return (
91 |
101 |
102 | {header.column.getCanSort() ? (
103 | variant === "dropdown" ? (
104 |
105 | ) : (
106 |
124 | )
125 | ) : (
126 | flexRender(header.column.columnDef.header, header.getContext())
127 | )}
128 |
129 |
130 | )
131 | }
132 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/filters/render-fields.tsx:
--------------------------------------------------------------------------------
1 | import { DatePicker } from "@/components/ui/date-picker"
2 | import { Input } from "@/components/ui/input"
3 | import { Label } from "@/components/ui/label"
4 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
5 | import type { Column, Table } from "@tanstack/react-table"
6 | import type React from "react"
7 | import { type ColumnFilter, type FilterType, filterTypes } from "./types"
8 | import { detectValueType, getColumnFilterTypesByValue } from "./utils"
9 |
10 | type UpdateFilterValueFn = (columnId: string, value: any) => void
11 |
12 | function renderFilterInput(
13 | table: Table,
14 | column: Column,
15 | filter: ColumnFilter,
16 | updateFilterValue: UpdateFilterValueFn,
17 | ) {
18 | const firstValue = table.getPreFilteredRowModel().flatRows[0]?.getValue(column.id)
19 | const type = detectValueType(firstValue)
20 |
21 | console.log("type", type, firstValue)
22 |
23 | switch (type) {
24 | case "number":
25 | if (filter.type === "between") {
26 | return (
27 |
28 |
32 | updateFilterValue(column.id, [
33 | e.target.value === "" ? null : Number(e.target.value),
34 | (filter.value as [number, number])?.[1],
35 | ])
36 | }
37 | className="w-[100px]"
38 | />
39 |
40 |
44 | updateFilterValue(column.id, [
45 | (filter.value as [number, number])?.[0],
46 | e.target.value === "" ? null : Number(e.target.value),
47 | ])
48 | }
49 | className="w-[100px]"
50 | />
51 |
52 | )
53 | }
54 | return (
55 | updateFilterValue(column.id, e.target.value === "" ? null : Number(e.target.value))}
59 | className="w-[200px]"
60 | />
61 | )
62 |
63 | case "date":
64 | if (filter.type === "dateBetween") {
65 | return (
66 |
67 | updateFilterValue(column.id, [date, (filter.value as [Date, Date])?.[1]])}
70 | />
71 |
72 | updateFilterValue(column.id, [(filter.value as [Date, Date])?.[0], date])}
75 | />
76 |
77 | )
78 | }
79 | return updateFilterValue(column.id, date)} />
80 |
81 | default:
82 | return (
83 | updateFilterValue(column.id, e.target.value)}
86 | className="w-[200px]"
87 | />
88 | )
89 | }
90 | }
91 |
92 | interface RenderFiltersProps {
93 | table: Table
94 | filters: ColumnFilter[]
95 | setFilters: React.Dispatch>
96 | updateFilterValue: UpdateFilterValueFn
97 | }
98 |
99 | export function RenderFilters({ table, filters, setFilters, updateFilterValue }: RenderFiltersProps) {
100 | const firstRow = table.getPrePaginationRowModel().rows?.[0]
101 |
102 | return (
103 |
104 | {table
105 | .getAllColumns()
106 | .filter((column) => column.getCanFilter())
107 | .map((column) => {
108 | const sampleValue = firstRow ? firstRow.getValue(column.id) : null
109 | const filter = filters.find((f) => f.id === column.id)
110 |
111 | const availableFilterTypes = getColumnFilterTypesByValue(sampleValue, filter?.type)
112 |
113 | const currentFilter = filters.find((f) => f.id === column.id) || {
114 | id: column.id,
115 | value: "",
116 | type: availableFilterTypes[0] ?? "contains",
117 | }
118 |
119 | return (
120 |
121 |
122 |
123 |
124 |
149 |
150 | {renderFilterInput(table, column, currentFilter, updateFilterValue)}
151 |
152 |
153 | )
154 | })}
155 |
156 | )
157 | }
158 |
--------------------------------------------------------------------------------
/src/components/ui/select.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as SelectPrimitive from "@radix-ui/react-select"
5 | import { Check, ChevronDown, ChevronUp } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const Select = SelectPrimitive.Root
10 |
11 | const SelectGroup = SelectPrimitive.Group
12 |
13 | const SelectValue = SelectPrimitive.Value
14 |
15 | const SelectTrigger = React.forwardRef<
16 | React.ElementRef,
17 | React.ComponentPropsWithoutRef
18 | >(({ className, children, ...props }, ref) => (
19 | span]:line-clamp-1",
23 | className,
24 | )}
25 | {...props}
26 | >
27 | {children}
28 |
29 |
30 |
31 |
32 | ))
33 | SelectTrigger.displayName = SelectPrimitive.Trigger.displayName
34 |
35 | const SelectScrollUpButton = React.forwardRef<
36 | React.ElementRef,
37 | React.ComponentPropsWithoutRef
38 | >(({ className, ...props }, ref) => (
39 |
44 |
45 |
46 | ))
47 | SelectScrollUpButton.displayName = SelectPrimitive.ScrollUpButton.displayName
48 |
49 | const SelectScrollDownButton = React.forwardRef<
50 | React.ElementRef,
51 | React.ComponentPropsWithoutRef
52 | >(({ className, ...props }, ref) => (
53 |
58 |
59 |
60 | ))
61 | SelectScrollDownButton.displayName = SelectPrimitive.ScrollDownButton.displayName
62 |
63 | const SelectContent = React.forwardRef<
64 | React.ElementRef,
65 | React.ComponentPropsWithoutRef
66 | >(({ className, children, position = "popper", ...props }, ref) => (
67 |
68 |
79 |
80 |
87 | {children}
88 |
89 |
90 |
91 |
92 | ))
93 | SelectContent.displayName = SelectPrimitive.Content.displayName
94 |
95 | const SelectLabel = React.forwardRef<
96 | React.ElementRef,
97 | React.ComponentPropsWithoutRef
98 | >(({ className, ...props }, ref) => (
99 |
100 | ))
101 | SelectLabel.displayName = SelectPrimitive.Label.displayName
102 |
103 | const SelectItem = React.forwardRef<
104 | React.ElementRef,
105 | React.ComponentPropsWithoutRef
106 | >(({ className, children, ...props }, ref) => (
107 |
115 |
116 |
117 |
118 |
119 |
120 | {children}
121 |
122 | ))
123 | SelectItem.displayName = SelectPrimitive.Item.displayName
124 |
125 | const SelectSeparator = React.forwardRef<
126 | React.ElementRef,
127 | React.ComponentPropsWithoutRef
128 | >(({ className, ...props }, ref) => (
129 |
130 | ))
131 | SelectSeparator.displayName = SelectPrimitive.Separator.displayName
132 |
133 | export {
134 | Select,
135 | SelectGroup,
136 | SelectValue,
137 | SelectTrigger,
138 | SelectContent,
139 | SelectLabel,
140 | SelectItem,
141 | SelectSeparator,
142 | SelectScrollUpButton,
143 | SelectScrollDownButton,
144 | }
145 |
--------------------------------------------------------------------------------
/src/app/(home)/_components/examples.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import { EnhancedTable } from "@/components/enhanced-table/composition-pattern"
4 | import { Button } from "@/components/ui/button"
5 | import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
6 | import { Input } from "@/components/ui/input"
7 | import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
8 | import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
9 | import type { Row } from "@tanstack/react-table"
10 | import { useState } from "react"
11 | import TableSkeleton from "../loading"
12 | import { type Person, columns } from "./columns"
13 |
14 | interface ExamplesProps {
15 | initialData: Person[]
16 | }
17 |
18 | export function Examples({ initialData }: ExamplesProps) {
19 | const [data, setData] = useState(initialData)
20 | const [dataCount, setDataCount] = useState(1000)
21 | const [headerVariant, setHeaderVariant] = useState<"default" | "dropdown">("default")
22 | const [isLoading, setIsLoading] = useState(false)
23 |
24 | async function fetchData() {
25 | setIsLoading(true)
26 | const newData = await fetch(`http://localhost:3000/api/fake-data?count=${dataCount}`, {
27 | cache: "no-cache",
28 | }).then((res) => res.json())
29 | setData(newData)
30 | setIsLoading(false)
31 | }
32 |
33 | return (
34 |
35 |
36 | setDataCount(Number(e.target.value))}
40 | className="w-32"
41 | />
42 |
43 |
44 | {isLoading ? "Loading..." : "Refresh Data"}
45 |
46 |
47 |
48 | {!isLoading ? (
49 |
50 |
51 | Full Featured
52 | Sortable Columns
53 | Reorderable Rows
54 |
55 |
56 |
57 |
58 | Full Featured Table
59 |
60 | This example showcases all available features of the EnhancedTable component.
61 |
62 |
63 |
64 |
72 |
73 |
74 |
75 |
76 |
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | Sortable Columns Table
111 |
112 | This example demonstrates a table with sortable columns without row reordering.
113 |
114 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | Reorderable Rows Table
135 |
136 | This example shows a table with reorderable rows without column sorting.
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 |
145 |
146 |
147 |
148 |
149 |
150 |
151 |
152 |
153 |
154 |
155 |
156 | ) : (
157 |
158 | )}
159 |
160 | )
161 | }
162 |
163 | const customRowStyles = (row: Row) => {
164 | const baseStyles = "transition-colors hover:bg-opacity-20"
165 | const statusStyles = {
166 | active: "hover:bg-green-100 dark:hover:bg-green-900/50",
167 | inactive: "hover:bg-red-100 dark:hover:bg-red-900/50",
168 | pending: "hover:bg-yellow-100 dark:hover:bg-yellow-900/50",
169 | }
170 |
171 | return `${baseStyles} ${statusStyles[row.original.status]}`
172 | }
173 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/body/index.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { TableCell, TableRow, TableBody as UiTableBody } from "@/components/ui/table"
3 | import { cn } from "@/lib/utils"
4 | import {
5 | SortableContext,
6 | horizontalListSortingStrategy,
7 | useSortable,
8 | verticalListSortingStrategy,
9 | } from "@dnd-kit/sortable"
10 | import { CSS } from "@dnd-kit/utilities"
11 | import { type Cell, type Row, flexRender } from "@tanstack/react-table"
12 | import { Edit2 } from "lucide-react"
13 | import React, { type CSSProperties, useState } from "react"
14 | import { useTableContext } from "../../table-context"
15 | import { getAlignment } from "../utils"
16 | import { TableRowEditor } from "./row-editor"
17 |
18 | interface TableBodyProps {
19 | customRowStyles?: (row: Row) => string
20 | }
21 |
22 | export function TableBody({ customRowStyles }: TableBodyProps) {
23 | const [editingRowId, setEditingRowId] = useState(null)
24 |
25 | const { table, updateData, enableEditing, columnOrder, enableColumnReorder, enableRowReorder, dataIds } =
26 | useTableContext()
27 |
28 | const handleEdit = (row: Row) => {
29 | setEditingRowId(row.id)
30 | }
31 |
32 | const handleSave = (rowIndex: number, updatedData: any) => {
33 | updateData(rowIndex, updatedData)
34 | setEditingRowId(null)
35 | }
36 |
37 | const handleCancel = () => {
38 | setEditingRowId(null)
39 | }
40 |
41 | const renderRow = (row: Row) => {
42 | const rowStyle = customRowStyles ? customRowStyles(row) : ""
43 | const depth = row.depth || 0
44 |
45 | if (editingRowId === row.id) {
46 | return (
47 |
48 |
49 |
50 | )
51 | }
52 |
53 | return (
54 |
55 |
59 | {enableColumnReorder && columnOrder ? (
60 |
61 | {row.getVisibleCells().map((cell, cellIndex) => (
62 |
71 | ))}
72 |
73 | ) : enableRowReorder && dataIds ? (
74 |
75 | {row.getVisibleCells().map((cell, cellIndex) => (
76 |
86 | ))}
87 |
88 | ) : (
89 | row.getVisibleCells().map((cell, cellIndex) => (
90 |
95 |
103 |
104 | ))
105 | )}
106 |
107 |
108 | )
109 | }
110 |
111 | return (
112 |
113 | {table.getRowModel().rows?.length ? (
114 | table.getRowModel().rows.map((row) => renderRow(row))
115 | ) : (
116 |
117 |
118 | No results.
119 |
120 |
121 | )}
122 |
123 | )
124 | }
125 |
126 | interface DraggableTableCellProps {
127 | cell: Cell
128 | cellIndex: number
129 | depth: number
130 | handleEdit: (row: Row) => void
131 | enableEditing: boolean | undefined
132 | row: Row
133 | }
134 |
135 | function DraggableTableCell({ cell, cellIndex, depth, handleEdit, enableEditing, row }: DraggableTableCellProps) {
136 | const { isDragging, setNodeRef, transform } = useSortable({
137 | id: cell.column.id,
138 | })
139 |
140 | const style: CSSProperties = {
141 | opacity: isDragging ? 0.8 : 1,
142 | position: "relative",
143 | transform: transform ? CSS.Translate.toString(transform) : undefined,
144 | transition: "width transform 0.2s ease-in-out",
145 | width: cell.column.getSize(),
146 | zIndex: isDragging ? 1 : 0,
147 | }
148 |
149 | return (
150 |
156 |
164 |
165 | )
166 | }
167 |
168 | interface DraggableRowProps {
169 | cell: Cell
170 | id: string
171 | cellIndex: number
172 | depth: number
173 | handleEdit: (row: Row) => void
174 | enableEditing: boolean | undefined
175 | row: Row
176 | }
177 |
178 | function DraggableRow({ cell, id, cellIndex, depth, enableEditing, handleEdit, row }: DraggableRowProps) {
179 | const { transform, transition, setNodeRef, isDragging } = useSortable({
180 | id,
181 | })
182 |
183 | const style: CSSProperties = {
184 | transform: CSS.Transform.toString(transform),
185 | transition: transition,
186 | opacity: isDragging ? 0.8 : 1,
187 | zIndex: isDragging ? 1 : 0,
188 | position: "relative",
189 | width: cell.column.getSize(),
190 | }
191 |
192 | return (
193 |
199 |
207 |
208 | )
209 | }
210 |
211 | interface DefaultTableCellProps {
212 | cell: Cell
213 | cellIndex: number
214 | depth: number
215 | handleEdit: (row: Row) => void
216 | enableEditing: boolean | undefined
217 | row: Row
218 | }
219 |
220 | export function DefaultTableCell({ cell, cellIndex, depth, handleEdit, enableEditing, row }: DefaultTableCellProps) {
221 | return (
222 | <>
223 | {cellIndex === 0 ? (
224 |
225 | {flexRender(cell.column.columnDef.cell, cell.getContext())}
226 |
227 | {enableEditing && (
228 | handleEdit(row)}>
229 |
230 |
231 | )}
232 |
233 | ) : (
234 | flexRender(cell.column.columnDef.cell, cell.getContext())
235 | )}
236 | >
237 | )
238 | }
239 |
--------------------------------------------------------------------------------
/src/components/ui/dropdown-menu.tsx:
--------------------------------------------------------------------------------
1 | "use client"
2 |
3 | import * as React from "react"
4 | import * as DropdownMenuPrimitive from "@radix-ui/react-dropdown-menu"
5 | import { Check, ChevronRight, Circle } from "lucide-react"
6 |
7 | import { cn } from "@/lib/utils"
8 |
9 | const DropdownMenu = DropdownMenuPrimitive.Root
10 |
11 | const DropdownMenuTrigger = DropdownMenuPrimitive.Trigger
12 |
13 | const DropdownMenuGroup = DropdownMenuPrimitive.Group
14 |
15 | const DropdownMenuPortal = DropdownMenuPrimitive.Portal
16 |
17 | const DropdownMenuSub = DropdownMenuPrimitive.Sub
18 |
19 | const DropdownMenuRadioGroup = DropdownMenuPrimitive.RadioGroup
20 |
21 | const DropdownMenuSubTrigger = React.forwardRef<
22 | React.ElementRef,
23 | React.ComponentPropsWithoutRef & {
24 | inset?: boolean
25 | }
26 | >(({ className, inset, children, ...props }, ref) => (
27 |
36 | {children}
37 |
38 |
39 | ))
40 | DropdownMenuSubTrigger.displayName = DropdownMenuPrimitive.SubTrigger.displayName
41 |
42 | const DropdownMenuSubContent = React.forwardRef<
43 | React.ElementRef,
44 | React.ComponentPropsWithoutRef
45 | >(({ className, ...props }, ref) => (
46 |
54 | ))
55 | DropdownMenuSubContent.displayName = DropdownMenuPrimitive.SubContent.displayName
56 |
57 | const DropdownMenuContent = React.forwardRef<
58 | React.ElementRef,
59 | React.ComponentPropsWithoutRef
60 | >(({ className, sideOffset = 4, ...props }, ref) => (
61 |
62 |
72 |
73 | ))
74 | DropdownMenuContent.displayName = DropdownMenuPrimitive.Content.displayName
75 |
76 | const DropdownMenuItem = React.forwardRef<
77 | React.ElementRef,
78 | React.ComponentPropsWithoutRef & {
79 | inset?: boolean
80 | }
81 | >(({ className, inset, ...props }, ref) => (
82 | svg]:size-4 [&>svg]:shrink-0",
86 | inset && "pl-8",
87 | className,
88 | )}
89 | {...props}
90 | />
91 | ))
92 | DropdownMenuItem.displayName = DropdownMenuPrimitive.Item.displayName
93 |
94 | const DropdownMenuCheckboxItem = React.forwardRef<
95 | React.ElementRef,
96 | React.ComponentPropsWithoutRef
97 | >(({ className, children, checked, ...props }, ref) => (
98 |
107 |
108 |
109 |
110 |
111 |
112 | {children}
113 |
114 | ))
115 | DropdownMenuCheckboxItem.displayName = DropdownMenuPrimitive.CheckboxItem.displayName
116 |
117 | const DropdownMenuRadioItem = React.forwardRef<
118 | React.ElementRef,
119 | React.ComponentPropsWithoutRef
120 | >(({ className, children, ...props }, ref) => (
121 |
129 |
130 |
131 |
132 |
133 |
134 | {children}
135 |
136 | ))
137 | DropdownMenuRadioItem.displayName = DropdownMenuPrimitive.RadioItem.displayName
138 |
139 | const DropdownMenuLabel = React.forwardRef<
140 | React.ElementRef,
141 | React.ComponentPropsWithoutRef & {
142 | inset?: boolean
143 | }
144 | >(({ className, inset, ...props }, ref) => (
145 |
150 | ))
151 | DropdownMenuLabel.displayName = DropdownMenuPrimitive.Label.displayName
152 |
153 | const DropdownMenuSeparator = React.forwardRef<
154 | React.ElementRef,
155 | React.ComponentPropsWithoutRef
156 | >(({ className, ...props }, ref) => (
157 |
158 | ))
159 | DropdownMenuSeparator.displayName = DropdownMenuPrimitive.Separator.displayName
160 |
161 | const DropdownMenuShortcut = ({ className, ...props }: React.HTMLAttributes) => {
162 | return
163 | }
164 | DropdownMenuShortcut.displayName = "DropdownMenuShortcut"
165 |
166 | export {
167 | DropdownMenu,
168 | DropdownMenuTrigger,
169 | DropdownMenuContent,
170 | DropdownMenuItem,
171 | DropdownMenuCheckboxItem,
172 | DropdownMenuRadioItem,
173 | DropdownMenuLabel,
174 | DropdownMenuSeparator,
175 | DropdownMenuShortcut,
176 | DropdownMenuGroup,
177 | DropdownMenuPortal,
178 | DropdownMenuSub,
179 | DropdownMenuSubContent,
180 | DropdownMenuSubTrigger,
181 | DropdownMenuRadioGroup,
182 | }
183 |
--------------------------------------------------------------------------------
/src/components/enhanced-table/composition-pattern/root.tsx:
--------------------------------------------------------------------------------
1 | import { Button } from "@/components/ui/button"
2 | import { Checkbox } from "@/components/ui/checkbox"
3 | import { cn } from "@/lib/utils"
4 | import {
5 | DndContext,
6 | type DragEndEvent,
7 | KeyboardSensor,
8 | MouseSensor,
9 | TouchSensor,
10 | closestCenter,
11 | useSensor,
12 | useSensors,
13 | } from "@dnd-kit/core"
14 | import { restrictToHorizontalAxis, restrictToVerticalAxis } from "@dnd-kit/modifiers"
15 | import { arrayMove, useSortable } from "@dnd-kit/sortable"
16 | import {
17 | type ColumnDef,
18 | type ColumnFiltersState,
19 | type ExpandedState,
20 | type PaginationState,
21 | type RowSelectionState,
22 | type SortingState,
23 | getCoreRowModel,
24 | getExpandedRowModel,
25 | getFilteredRowModel,
26 | getPaginationRowModel,
27 | getSortedRowModel,
28 | useReactTable,
29 | } from "@tanstack/react-table"
30 | import { ChevronDown, ChevronRight, GripVertical } from "lucide-react"
31 | import React from "react"
32 | import { TableProvider } from "../table-context"
33 | import { filterRows } from "./filters/utils"
34 | import type { TableRootProps } from "./types"
35 | import { isSpecialId } from "./utils"
36 |
37 | export function TableRoot({
38 | data,
39 | columns,
40 | enableExpansion,
41 | enableSelection,
42 | enableEditing,
43 | enableColumnReorder,
44 | enableRowReorder,
45 | rowReorderKey,
46 | children,
47 | }: TableRootProps) {
48 | const [rowSelection, setRowSelection] = React.useState({})
49 | const [sorting, setSorting] = React.useState([])
50 | const [columnFilters, setColumnFilters] = React.useState([])
51 | const [expanded, setExpanded] = React.useState({})
52 | const [pagination, setPagination] = React.useState({ pageIndex: 0, pageSize: 10 })
53 | const [tableData, setTableData] = React.useState(data)
54 |
55 | React.useEffect(() => {
56 | setTableData(data)
57 | }, [data])
58 |
59 | const updateData = (rowIndex: number, updatedData: TData) => {
60 | setTableData((prevData) => {
61 | const newData = [...prevData]
62 | newData[rowIndex] = updatedData
63 | return newData
64 | })
65 | }
66 |
67 | const dataIds = React.useMemo(
68 | () => tableData.map((data) => (data as any)[rowReorderKey!]),
69 | [tableData, rowReorderKey],
70 | )
71 |
72 | const memoColumns = React.useMemo(() => {
73 | let newColumns = [...columns]
74 |
75 | if (enableSelection && enableExpansion) {
76 | newColumns = [
77 | {
78 | id: "select-expand",
79 | header: ({ table }) => (
80 |
81 | table.toggleAllPageRowsSelected(!!value)}
84 | aria-label="Select all"
85 | />
86 | table.toggleAllRowsExpanded()}>
87 | {table.getIsAllRowsExpanded() ? (
88 |
89 | ) : (
90 |
91 | )}
92 |
93 |
94 | ),
95 | cell: ({ row }) => (
96 |
97 | row.toggleSelected(!!value)}
100 | aria-label="Select row"
101 | />
102 | {row.getCanExpand() && (
103 | row.toggleExpanded()}>
104 | {row.getIsExpanded() ? : }
105 |
106 | )}
107 |
108 | ),
109 | enableSorting: false,
110 | enableHiding: false,
111 | } as ColumnDef,
112 | ...newColumns,
113 | ]
114 | } else if (enableSelection && !enableExpansion) {
115 | newColumns = [
116 | {
117 | id: "select",
118 | header: ({ table }) => (
119 | table.toggleAllPageRowsSelected(!!value)}
122 | aria-label="Select all"
123 | />
124 | ),
125 | cell: ({ row }) => (
126 | row.toggleSelected(!!value)}
129 | aria-label="Select row"
130 | />
131 | ),
132 | enableSorting: false,
133 | enableHiding: false,
134 | } as ColumnDef,
135 | ...newColumns,
136 | ]
137 | } else if (enableExpansion && !enableSelection) {
138 | newColumns = [
139 | {
140 | id: "expand",
141 | header: ({ table }) => (
142 | table.toggleAllRowsExpanded()}>
143 | {table.getIsAllRowsExpanded() ? (
144 |
145 | ) : (
146 |
147 | )}
148 |
149 | ),
150 | cell: ({ row }) =>
151 | row.getCanExpand() ? (
152 | row.toggleExpanded()}>
153 | {row.getIsExpanded() ? : }
154 |
155 | ) : null,
156 | enableSorting: false,
157 | enableHiding: false,
158 | } as ColumnDef,
159 | ...newColumns,
160 | ]
161 | }
162 |
163 | if (enableRowReorder) {
164 | newColumns = [
165 | ...newColumns,
166 | {
167 | id: "reorder",
168 | header: () => null,
169 | cell: ({ row, table }) => !table.getIsSomeRowsExpanded() && ,
170 | enableSorting: false,
171 | enableHiding: false,
172 | } as ColumnDef,
173 | ]
174 | }
175 |
176 | return newColumns
177 | }, [columns, enableExpansion, enableSelection, enableRowReorder])
178 |
179 | const [columnOrder, setColumnOrder] = React.useState(() =>
180 | memoColumns.map((column) => (enableColumnReorder && column.id && !enableRowReorder ? column.id : "")),
181 | )
182 |
183 | const table = useReactTable({
184 | data: tableData,
185 | columns: memoColumns,
186 | getCoreRowModel: getCoreRowModel(),
187 | onRowSelectionChange: setRowSelection,
188 | onSortingChange: setSorting,
189 | getSortedRowModel: getSortedRowModel(),
190 | onColumnFiltersChange: setColumnFilters,
191 | getFilteredRowModel: getFilteredRowModel(),
192 | getPaginationRowModel: getPaginationRowModel(),
193 | onPaginationChange: setPagination,
194 | onExpandedChange: setExpanded,
195 | getExpandedRowModel: getExpandedRowModel(),
196 | getSubRows: (row: any) => row.subRows,
197 | getRowId: (row: any) => (row as any)[rowReorderKey!],
198 | state: {
199 | rowSelection,
200 | sorting,
201 | columnFilters,
202 | pagination,
203 | expanded,
204 | columnOrder,
205 | },
206 | onColumnOrderChange: setColumnOrder,
207 | filterFns: {
208 | filterRows: filterRows,
209 | },
210 | })
211 |
212 | function handleDragEnd(event: DragEndEvent) {
213 | const { active, over } = event
214 | if (!active || !over || active.id === over.id) return
215 |
216 | const activeId = active.id.toString()
217 | const overId = over.id.toString()
218 |
219 | if (isSpecialId(activeId) || isSpecialId(overId)) return
220 |
221 | if (enableColumnReorder) {
222 | setColumnOrder((current) => {
223 | const oldIndex = current.indexOf(activeId)
224 | const newIndex = current.indexOf(overId)
225 | return arrayMove(current, oldIndex, newIndex)
226 | })
227 | }
228 |
229 | if (enableRowReorder) {
230 | setTableData((prevData) => {
231 | const oldIndex = dataIds.indexOf(activeId)
232 | const newIndex = dataIds.indexOf(overId)
233 | return arrayMove(prevData, oldIndex, newIndex)
234 | })
235 | }
236 | }
237 |
238 | const sensors = useSensors(useSensor(MouseSensor, {}), useSensor(TouchSensor, {}), useSensor(KeyboardSensor, {}))
239 |
240 | if (enableColumnReorder) {
241 | return (
242 |
248 |
255 | {children}
256 |
257 |
258 | )
259 | }
260 |
261 | if (enableRowReorder) {
262 | return (
263 |
269 |
277 | {children}
278 |
279 |
280 | )
281 | }
282 |
283 | return (
284 |
285 | {children}
286 |
287 | )
288 | }
289 |
290 | const RowDragHandleCell = ({ rowId }: { rowId: string }) => {
291 | const { isDragging, attributes, listeners } = useSortable({
292 | id: rowId,
293 | })
294 |
295 | return (
296 |
302 |
303 |
304 | )
305 | }
306 |
--------------------------------------------------------------------------------
|