import type { Meta, StoryObj } from "@storybook/react" import { useCallback, useEffect, useState } from "react" import mockData from "./__mocks__/mockData.json" import { DataTable } from "./DataTable" import type { DataTableColumn, DataTableFilter } from "./DataTable.types" import { TableCell, TableRow } from "./Table" const meta: Meta = { title: "Table/DataTable", component: DataTable, parameters: { layout: "padded", }, tags: ["autodocs"], } export default meta type User = { id: number name: string email: string role: string status: string department: string } type Story = StoryObj type UserStory = StoryObj> type CustomerStory = StoryObj> const users: User[] = [ { id: 1, name: "John Doe", email: "john@example.com", role: "Admin", status: "Active", department: "Engineering", }, { id: 2, name: "Jane Smith", email: "jane@example.com", role: "User", status: "Active", department: "Marketing", }, { id: 3, name: "Bob Johnson", email: "bob@example.com", role: "User", status: "Inactive", department: "Sales", }, { id: 4, name: "Alice Brown", email: "alice@example.com", role: "Admin", status: "Active", department: "Engineering", }, { id: 5, name: "Charlie Wilson", email: "charlie@example.com", role: "User", status: "Active", department: "HR", }, { id: 6, name: "Diana Davis", email: "diana@example.com", role: "User", status: "Inactive", department: "Marketing", }, { id: 7, name: "Eve Miller", email: "eve@example.com", role: "Admin", status: "Active", department: "Sales", }, { id: 8, name: "Frank Garcia", email: "frank@example.com", role: "User", status: "Active", department: "Engineering", }, { id: 9, name: "Grace Lee", email: "grace@example.com", role: "User", status: "Inactive", department: "HR", }, { id: 10, name: "Henry Taylor", email: "henry@example.com", role: "Admin", status: "Active", department: "Marketing", }, ] const columns: DataTableColumn[] = [ { accessorKey: "name", header: "Name", cell: info => info.getValue(), enableSorting: true, enableHiding: true, }, { accessorKey: "email", header: "Email", cell: info => info.getValue(), enableHiding: true, }, { accessorKey: "role", header: "Role", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, { accessorKey: "status", header: "Status", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, { accessorKey: "department", header: "Department", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, ] export const Basic: UserStory = { args: { data: users, columns, showColumnVisibility: true, }, } export const WithRowSelection: UserStory = { args: { data: users, columns, selectedRowActions: [ { label: "Delete Selected", onClick: selectedRows => alert(`Delete ${selectedRows.length} rows`), variant: "destructive", }, { label: "Export Selected", onClick: selectedRows => alert(`Export ${selectedRows.length} rows`), variant: "outline", }, ], }, } export const CustomRendering: UserStory = { args: { data: users, columns: [ { accessorKey: "name", header: "Name", cell: info => info.getValue(), enableSorting: true, }, { accessorKey: "email", header: "Email", cell: info => info.getValue(), enableSorting: false, }, { accessorKey: "role", header: "Role", cell: info => ( {info.getValue() as string} ), enableSorting: false, }, { accessorKey: "status", header: "Status", cell: info => ( {info.getValue() as string} ), enableSorting: false, }, { accessorKey: "department", header: "Department", cell: info => info.getValue(), enableSorting: false, }, ] as DataTableColumn[], }, } export const EmptyState: UserStory = { args: { data: [], columns, emptyMessage: "No users found. Try adjusting your search or filters.", }, } export const WithColumnVisibility: Story = { args: { data: users, columns: [ { accessorKey: "name", header: "Name", cell: info => info.getValue(), enableSorting: true, enableHiding: true, }, { accessorKey: "email", header: "Email", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, { accessorKey: "role", header: "Role", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, { accessorKey: "status", header: "Status", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, { accessorKey: "department", header: "Department", cell: info => info.getValue(), enableSorting: false, enableHiding: true, }, ], showColumnVisibility: true, }, parameters: { docs: { description: { story: "This example demonstrates column visibility functionality. You can toggle columns on/off using the 'View' button in the toolbar. The checkboxes should update correctly when toggled.", }, }, }, } export const CustomStyling: Story = { render: args => { return ( (row.index % 2 === 0 ? "bg-blue-50" : "bg-white")} cellClassName={cell => cell.column.id === "name" ? "font-bold text-blue-700" : "text-gray-700" } footerClassName="bg-blue-100 text-blue-800 text-center" /> ) }, parameters: { docs: { description: { story: "This example demonstrates how to use DataTable's className, tableClassName, headerClassName, rowClassName, cellClassName, and footerClassName props for custom styling. Header, rows, cells, and footer all have different styles applied.", }, }, }, } export const WithFooter: UserStory = { render: args => { const total = args.data ? args.data.length : 0 return ( Total {total} } /> ) }, args: { data: users, columns, searchPlaceholder: "Search users...", }, parameters: { docs: { description: { story: "This example demonstrates passing a custom footer row to the DataTable using the footer prop.", }, }, }, } // Customer Management with exact data from image type Customer = { id: string name: string company: string notes: string status: "purchased" | "potential" | "researching" budget: string interests: string[] interestsOverflow?: number phone: string phoneOverflow?: number email: string address: string lastContact: string } const customerColumns: DataTableColumn[] = [ { accessorKey: "name", header: "Khách hàng", width: 240, cell: info => (
{info.getValue() as string} {info.row.original.company}
), enableSorting: false, }, { accessorKey: "notes", header: "Ghi chú", width: 240, cell: info => info.getValue(), enableSorting: false, }, { accessorKey: "status", header: "Trạng thái", width: 136, cell: info => { const status = info.getValue() as string const statusConfig = { purchased: { text: "Đã mua", className: "bg-[#0047F112] text-[#002BB7C5]" }, potential: { text: "Tiềm năng", className: "bg-[#00A43319] text-[#00713FDE]" }, researching: { text: "Đang tìm hiểu", className: "bg-[#FFDE003D] text-[#AB6400]" }, } const config = statusConfig[status as keyof typeof statusConfig] return ( {config.text} ) }, enableSorting: false, }, { accessorKey: "budget", header: "Ngân sách", width: 160, cell: info => info.getValue(), enableSorting: false, }, { accessorKey: "interests", header: "Quan tâm", width: 200, cell: info => { const interests = info.getValue() as string[] const customer = info.row.original const maxVisible = 2 const visibleTags = interests.slice(0, maxVisible) const hiddenCount = customer.interestsOverflow || 0 return (
{visibleTags.map((tag, index) => ( {tag} ))} {hiddenCount > 0 && ( +{hiddenCount} )}
) }, enableSorting: false, }, { accessorKey: "phone", header: "Số điện thoại", width: 200, cell: info => { const customer = info.row.original return (
{info.getValue() as string} {customer.phoneOverflow && customer.phoneOverflow > 0 && ( +{customer.phoneOverflow} )}
) }, enableSorting: false, }, // Hidden columns that require horizontal scrolling { accessorKey: "email", header: "Email", width: 215, cell: info => ( ), enableSorting: false, }, { accessorKey: "address", header: "Địa chỉ", width: 330, cell: info => (
{info.getValue() as string}
), enableSorting: false, }, { accessorKey: "lastContact", header: "Liên hệ lần cuối", width: 200, cell: info =>
{info.getValue() as string}
, enableSorting: false, }, ] export const WithAPIFiltersAndPagination: CustomerStory = { render: args => { // Custom React node icons const CustomEyeIcon = () => ( ) const CustomEditIcon = () => ( ) const CustomEmailIcon = () => ( ) const CustomTrashIcon = () => ( ) // Create a proper React component to use hooks const CustomerAPIFiltersAndPaginationComponent = () => { const [filters, setFilters] = useState([ { key: "status", label: "Trạng thái", options: mockData.filters.status, value: [], }, { key: "budget", label: "Ngân sách", options: mockData.filters.budget, value: [], }, { key: "interests", label: "Quan tâm", options: mockData.filters.interests, value: [], }, ]) const [data, setData] = useState([]) const [totalItems, setTotalItems] = useState(0) const [loading, setLoading] = useState(false) const [pageIndex, setPageIndex] = useState(0) const [pageSize, setPageSize] = useState(5) const [search, setSearch] = useState("") const searchKey = "name" // Define which field to search in the API // Simulate API call useEffect(() => { setLoading(true) const timeout = setTimeout(() => { let filtered = mockData.customers as Customer[] // Apply filters for (const f of filters) { const values = f.value || [] if (values.length > 0) { filtered = filtered.filter(customer => { if (f.key === "interests") { // For interests, check if any of the customer's interests match the filter return customer.interests.some(interest => values.includes(interest)) } return values.includes(customer[f.key as keyof Customer] as string) }) } } // Apply server-side search based on searchKey if (search) { filtered = filtered.filter(customer => { const searchValue = search.toLowerCase() const fieldValue = customer[searchKey as keyof Customer] if (fieldValue === undefined) return false if (Array.isArray(fieldValue)) { return fieldValue.some(item => String(item).toLowerCase().includes(searchValue)) } return String(fieldValue).toLowerCase().includes(searchValue) }) } const total = filtered.length const start = pageIndex * pageSize const paged = filtered.slice(start, start + pageSize) setData(paged) setTotalItems(total) setLoading(false) }, 500) return () => clearTimeout(timeout) }, [filters, pageIndex, pageSize, search]) const controlledFilters = filters.map(filter => ({ ...filter, onValueChange: value => { setFilters(prev => prev.map(f => (f.key === filter.key ? { ...f, value } : f))) setPageIndex(0) }, })) return ( alert(`Xem chi tiết: ${customer.name}`), icon: , }, { label: "Chỉnh sửa", onClick: customer => alert(`Chỉnh sửa: ${customer.name}`), icon: , }, { label: "Gửi email", onClick: customer => alert(`Gửi email đến: ${customer.email}`), icon: , }, { label: "Xóa", onClick: customer => alert(`Xóa khách hàng: ${customer.name}`), variant: "destructive", icon: , }, ]} selectedRowActions={[ { label: "Xuất dữ liệu", onClick: selectedRows => alert(`Xuất ${selectedRows.length} khách hàng`), variant: "outline", }, { label: "Xóa đã chọn", onClick: selectedRows => alert(`Xóa ${selectedRows.length} khách hàng`), variant: "destructive", }, ]} /> ) } return (
) }, parameters: { docs: { description: { story: "This example demonstrates a customer management table with server-side filtering and pagination. It shows how to handle complex data structures with nested properties and arrays in a server-side filtering scenario.", }, }, }, } export const WithMixedSelection: Story = { render: () => { const [selectedRows, setSelectedRows] = useState([]) const [rowSelection, setRowSelection] = useState>({}) const handleRowSelectionChange = useCallback( (selection: Record, selectedRowsData: typeof users) => { setRowSelection(selection) setSelectedRows(selectedRowsData) }, [], ) return (
{/* External Components */} {selectedRows.length > 0 && (
🎯 External Selection: {selectedRows.length} user{selectedRows.length > 1 ? "s" : ""}{" "} selected
Names: {selectedRows.map(user => user.name).join(", ")}
)} {/* DataTable with BOTH external and built-in selection */} alert(`Built-in export: ${selectedRows.length} users`), variant: "outline", }, { label: "Built-in Delete", onClick: selectedRows => alert(`Built-in delete: ${selectedRows.length} users`), variant: "destructive", }, ]} />
) }, parameters: { docs: { description: { story: "This example shows how to use both external and built-in selection together. " + "By setting `showBuiltInSelection={true}`, you can explicitly show the built-in selection " + "even when using external selection callbacks. This gives you maximum flexibility - " + "external components for custom UI and built-in selection for quick actions.", }, }, }, }