import { CommandBar, Table, clx } from "@medusajs/ui" import { ColumnDef, Table as ReactTable, Row, flexRender, } from "@tanstack/react-table" import { ComponentPropsWithoutRef, Fragment, UIEvent, useEffect, useRef, useState, } from "react" import { useTranslation } from "react-i18next" import { Link } from "react-router-dom" import { NoResults } from "../../../common/empty-table-content" type BulkCommand = { label: string shortcut: string action: (selection: Record) => Promise } export interface DataTableRootProps { /** * The table instance to render */ table: ReactTable /** * The columns to render */ columns: ColumnDef[] /** * Function to generate a link to navigate to when clicking on a row */ navigateTo?: (row: Row) => string /** * Bulk actions to render */ commands?: BulkCommand[] /** * The total number of items in the table */ count?: number /** * Whether to display pagination controls */ pagination?: boolean /** * Whether the table is empty due to no results from the active query */ noResults?: boolean /** * Whether to display the tables header */ noHeader?: boolean /** * The layout of the table */ layout?: "fill" | "fit" } /** * TODO * * Add a sticky header to the table that shows the column name when scrolling through the table vertically. * * This is a bit tricky as we can't support horizontal scrolling and sticky headers at the same time, natively * with CSS. We need to implement a custom solution for this. One solution is to render a duplicate table header * using a DIV that, but it will require rerendeing the duplicate header every time the window is resized, to keep * the columns aligned. */ /** * Table component for rendering a table with pagination, filtering and ordering. */ export const DataTableRoot = ({ table, columns, pagination, navigateTo, commands, count = 0, noResults = false, noHeader = false, layout = "fit", }: DataTableRootProps) => { const { t } = useTranslation() const [showStickyBorder, setShowStickyBorder] = useState(false) const scrollableRef = useRef(null) const hasSelect = columns.find((c) => c.id === "select") const hasActions = columns.find((c) => c.id === "actions") const hasCommandBar = commands && commands.length > 0 const rowSelection = table.getState().rowSelection const { pageIndex, pageSize } = table.getState().pagination const colCount = columns.length - (hasSelect ? 1 : 0) - (hasActions ? 1 : 0) const colWidth = 100 / colCount const handleHorizontalScroll = (e: UIEvent) => { const scrollLeft = e.currentTarget.scrollLeft if (scrollLeft > 0) { setShowStickyBorder(true) } else { setShowStickyBorder(false) } } const handleAction = async (action: BulkCommand["action"]) => { await action(rowSelection).then(() => { table.resetRowSelection() }) } useEffect(() => { scrollableRef.current?.scroll({ top: 0, left: 0 }) }, [pageIndex]) return (
{!noResults ? ( {!noHeader && ( {table.getHeaderGroups().map((headerGroup) => { return ( {headerGroup.headers.map((header, index) => { const isActionHeader = header.id === "actions" const isSelectHeader = header.id === "select" const isSpecialHeader = isActionHeader || isSelectHeader const firstHeader = headerGroup.headers.findIndex( (h) => h.id !== "select" ) const isFirstHeader = firstHeader !== -1 ? header.id === headerGroup.headers[firstHeader].id : index === 0 const isStickyHeader = isSelectHeader || isFirstHeader return ( {flexRender( header.column.columnDef.header, header.getContext() )} ) })} ) })} )} {table.getRowModel().rows.map((row) => { const to = navigateTo ? navigateTo(row) : undefined const isRowDisabled = hasSelect && !row.getCanSelect() const isOdd = row.depth % 2 !== 0 const cells = row.getVisibleCells() return ( {cells.map((cell, index) => { const visibleCells = row.getVisibleCells() const isSelectCell = cell.column.id === "select" const firstCell = visibleCells.findIndex( (h) => h.column.id !== "select" ) const isFirstCell = firstCell !== -1 ? cell.column.id === visibleCells[firstCell].column.id : index === 0 const isStickyCell = isSelectCell || isFirstCell /** * If the table has nested rows, we need to offset the cell padding * to indicate the depth of the row. */ const depthOffset = row.depth > 0 && isFirstCell ? row.depth * 14 + 24 : undefined const hasLeftOffset = isStickyCell && hasSelect && !isSelectCell const Inner = flexRender( cell.column.columnDef.cell, cell.getContext() ) const isTabableLink = isFirstCell && !!to const shouldRenderAsLink = !!to && !isSelectCell return ( {shouldRenderAsLink ? (
{Inner}
) : ( Inner )}
) })}
) })}
) : (
)}
{pagination && (
)} {hasCommandBar && ( {t("general.countSelected", { count: Object.keys(rowSelection).length, })} {commands?.map((command, index) => { return ( handleAction(command.action)} /> {index < commands.length - 1 && } ) })} )}
) } type PaginationProps = Omit< ComponentPropsWithoutRef, "translations" > const Pagination = (props: PaginationProps) => { const { t } = useTranslation() const translations = { of: t("general.of"), results: t("general.results"), pages: t("general.pages"), prev: t("general.prev"), next: t("general.next"), } return ( ) }