"use client"; import type { Row } from "@tanstack/react-table"; import { flexRender } from "@tanstack/react-table"; import type { Table as ReactTableType, Header, HeaderGroup } from "@tanstack/react-table"; import { useVirtualizer, type Virtualizer, type VirtualItem } from "@tanstack/react-virtual"; import kebabCase from "lodash/kebabCase"; import { useEffect, useState, memo, useMemo } from "react"; import { useLocale } from "@calcom/lib/hooks/useLocale"; import classNames from "@calcom/ui/classNames"; import { Command, CommandList, CommandItem } from "@calcom/ui/components/command"; import { Icon } from "@calcom/ui/components/icon"; import { Popover, PopoverTrigger, PopoverContent } from "@calcom/ui/components/popover"; import { TableNew, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@calcom/ui/components/table"; import { useColumnSizingVars } from "../hooks"; import { useColumnResizing } from "../hooks/useColumnResizing"; import type { SeparatorRow } from "../lib/separator"; import { isSeparatorRow } from "../lib/separator"; export type DataTablePropsFromWrapper = { table: ReactTableType; tableContainerRef: React.RefObject; isPending?: boolean; variant?: "default" | "compact"; testId?: string; bodyTestId?: string; children?: React.ReactNode; enableColumnResizing?: boolean; className?: string; containerClassName?: string; headerClassName?: string; rowClassName?: string | ((row: Row) => string); rowTestId?: string | ((row: Row) => string | undefined); rowDataAttributes?: (row: Row) => Record | undefined; paginationMode?: "infinite" | "standard"; hasWrapperContext?: boolean; hideSeparatorsOnSort?: boolean; hideSeparatorsOnFilter?: boolean; separatorClassName?: string; }; export type DataTableProps = DataTablePropsFromWrapper & { onRowMouseclick?: (row: Row) => void; onScroll?: (e: Pick, "target">) => void; tableOverlay?: React.ReactNode; enableColumnResizing?: boolean; }; export function DataTable({ table, tableContainerRef, isPending, variant, onRowMouseclick, onScroll, children, enableColumnResizing, testId, bodyTestId, className, containerClassName, headerClassName, rowClassName, rowTestId, rowDataAttributes, paginationMode = "infinite", hasWrapperContext = false, hideSeparatorsOnSort = true, hideSeparatorsOnFilter = false, separatorClassName, ...rest }: DataTableProps & React.ComponentPropsWithoutRef<"div">) { const { rows } = table.getRowModel(); const rowVirtualizer = useVirtualizer({ count: rows.length, estimateSize: () => 100, getScrollElement: () => tableContainerRef.current, measureElement: typeof window !== "undefined" && navigator.userAgent.indexOf("Firefox") === -1 ? (element) => element?.getBoundingClientRect().height : undefined, overscan: 10, }); const virtualItemsCount = rowVirtualizer.getVirtualItems().length; useEffect(() => { if (paginationMode === "infinite" && virtualItemsCount >= rows.length && tableContainerRef.current) { const target = tableContainerRef.current; // Right after the last row is rendered, tableContainer's scrollHeight is // temporarily larger than the actual height of the table, so we need to // wait for a short time before calling onScroll to ensure the scrollHeight // is correct. setTimeout(() => { onScroll?.({ target }); }, 100); } }, [virtualItemsCount, rows.length, tableContainerRef.current, paginationMode, onScroll]); const columnSizingVars = useColumnSizingVars({ table }); useColumnResizing({ enabled: Boolean(enableColumnResizing), table, tableContainerRef, }); return (
{/* Invalidate left & right properties for <= sm screen size, because we pin columns only for >= sm screen sizes. */}
{table.getHeaderGroups().map((headerGroup: HeaderGroup) => ( {headerGroup.headers.map((header: Header) => { const { column } = header; return ( {Boolean(enableColumnResizing) && header.column.getCanResize() && (
)} ); })} ))} {/* When resizing any column we will render this special memoized version of our table body */} {table.getState().columnSizingInfo.isResizingColumn ? ( ) : ( )}
{children}
); } const MemoizedTableBody = memo( DataTableBody, (prev, next) => prev.table.options.data === next.table.options.data && prev.rowVirtualizer === next.rowVirtualizer && prev.rows === next.rows && prev.testId === next.testId && prev.variant === next.variant && prev.isPending === next.isPending && prev.onRowMouseclick === next.onRowMouseclick && prev.paginationMode === next.paginationMode && prev.rowClassName === next.rowClassName && prev.rowTestId === next.rowTestId && prev.rowDataAttributes === next.rowDataAttributes && prev.hideSeparatorsOnSort === next.hideSeparatorsOnSort && prev.hideSeparatorsOnFilter === next.hideSeparatorsOnFilter && prev.separatorClassName === next.separatorClassName && prev.tableContainerRef === next.tableContainerRef ) as typeof DataTableBody; type DataTableBodyProps = { table: ReactTableType; rowVirtualizer: Virtualizer; rows: Row[]; testId?: string; variant?: "default" | "compact"; isPending?: boolean; onRowMouseclick?: (row: Row) => void; paginationMode?: "infinite" | "standard"; rowClassName?: string | ((row: Row) => string); rowTestId?: string | ((row: Row) => string | undefined); rowDataAttributes?: (row: Row) => Record | undefined; hideSeparatorsOnSort?: boolean; hideSeparatorsOnFilter?: boolean; separatorClassName?: string; tableContainerRef: React.RefObject; }; type RowToRender = { row: Row; virtualItem?: VirtualItem; }; function SeparatorRowRenderer({ separator, className }: { separator: SeparatorRow; className?: string }) { return (
{separator.label}
); } function DataTableBody({ table, rowVirtualizer: _rowVirtualizer, rows, testId, variant, isPending, onRowMouseclick, paginationMode, rowClassName, rowTestId, rowDataAttributes, hideSeparatorsOnSort = true, hideSeparatorsOnFilter = false, separatorClassName, tableContainerRef, }: DataTableBodyProps & { paginationMode?: "infinite" | "standard" }) { const { t } = useLocale(); const hasActiveSorting = table.getState().sorting.length > 0; const hasActiveFilters = table.getState().columnFilters.length > 0; const filteredRows = useMemo(() => { if ((hideSeparatorsOnSort && hasActiveSorting) || (hideSeparatorsOnFilter && hasActiveFilters)) { return rows.filter((row) => !isSeparatorRow(row.original)); } return rows; }, [rows, hideSeparatorsOnSort, hideSeparatorsOnFilter, hasActiveSorting, hasActiveFilters]); const filteredRowVirtualizer = useVirtualizer({ count: filteredRows.length, estimateSize: () => 100, getScrollElement: () => tableContainerRef.current, measureElement: typeof window !== "undefined" && navigator.userAgent.indexOf("Firefox") === -1 ? (element) => element?.getBoundingClientRect().height : undefined, overscan: 10, }); const virtualItems = filteredRowVirtualizer.getVirtualItems(); const tableHeight = paginationMode === "infinite" ? filteredRowVirtualizer.getTotalSize() : "auto"; const rowsToRender = useMemo[]>(() => { return paginationMode === "infinite" ? virtualItems.map((virtualItem) => ({ row: filteredRows[virtualItem.index] as Row, virtualItem, })) : filteredRows.map((row) => ({ row })); }, [paginationMode, virtualItems, filteredRows]); if (!isPending && rowsToRender.length === 0) { return ( {t("no_results")} ); } return ( {rowsToRender.map(({ row, virtualItem }) => { const isSeparator = isSeparatorRow(row.original); if (isSeparator) { return ( filteredRowVirtualizer.measureElement(node) : undefined} key={row.id} data-index={virtualItem?.index} style={{ display: "flex", width: "100%", ...(virtualItem && { position: "absolute", transform: `translateY(${virtualItem.start}px)`, }), }} className="hover:bg-subtle border-muted flex w-full border-b"> ); } const computedRowTestId = typeof rowTestId === "function" ? rowTestId(row) : rowTestId; const computedRowClassName = typeof rowClassName === "function" ? rowClassName(row) : rowClassName; const computedDataAttributes = rowDataAttributes?.(row); return ( filteredRowVirtualizer.measureElement(node) : undefined} key={row.id} data-testid={computedRowTestId} {...computedDataAttributes} data-index={virtualItem?.index} // needed for dynamic row height measurement data-state={row.getIsSelected() && "selected"} onClick={(e) => { if (!onRowMouseclick) return; const target = e.target as Node | null; const current = e.currentTarget as HTMLElement | null; // Only invoke the handler when the event target is inside the row element. if (!target || !current || !current.contains(target)) return; onRowMouseclick(row); }} style={{ display: "flex", ...(virtualItem && { position: "absolute", transform: `translateY(${virtualItem.start}px)`, width: "100%", }), }} className={classNames(onRowMouseclick && "hover:cursor-pointer", "group", computedRowClassName)}> {row.getVisibleCells().map((cell) => { const column = cell.column; return ( {flexRender(cell.column.columnDef.cell, cell.getContext())} ); })} ); })} ); } const TableHeadLabel = ({ header }: { header: Header }) => { const [open, setOpen] = useState(false); const { t } = useLocale(); const canHide = header.column.getCanHide(); const canSort = header.column.getCanSort(); if (!canSort && !canHide) { if (typeof header.column.columnDef.header === "string") { return (
{header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())}
); } else { return header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext()); } } return ( {canSort && ( <> { if (header.column.getIsSorted() === "asc") { header.column.clearSorting(); } else { header.column.toggleSorting(false, true); } }}> {t("asc")}
{header.column.getIsSorted() === "asc" && } { if (header.column.getIsSorted() === "desc") { header.column.clearSorting(); } else { header.column.toggleSorting(true, true); } }}> {t("desc")}
{header.column.getIsSorted() === "desc" && } )} {canHide && ( { header.column.toggleVisibility(false); setOpen(false); }}> {t("hide")} )} ); };