Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: replace DataTable implementation #18785

Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
169 changes: 117 additions & 52 deletions packages/features/data-table/components/DataTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,33 @@ import { usePathname } from "next/navigation";
import { useEffect, memo } from "react";

import classNames from "@calcom/lib/classNames";
import { Icon, TableNew, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@calcom/ui";
import { Icon } from "@calcom/ui";

import { useColumnSizingVars } from "../hooks";
import { usePersistentColumnResizing } from "../lib/resizing";
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "./Table";

const getPinningStyles = (column: Column<Item>, isHeader: boolean): CSSProperties => {
const isPinned = column.getIsPinned();
let zIndex = 0;
if (isHeader && isPinned) {
zIndex = 20;
} else if (isHeader && !isPinned) {
zIndex = 10;
} else if (!isHeader && isPinned) {
zIndex = 1;
} else {
zIndex = 0;
}

return {
left: isPinned === "left" ? `${column.getStart("left")}px` : undefined,
right: isPinned === "right" ? `${column.getAfter("right")}px` : undefined,
position: isPinned ? "sticky" : "relative",
width: column.getSize(),
zIndex,
};
};

export type DataTableProps<TData, TValue> = {
table: ReactTableType<TData>;
Expand Down Expand Up @@ -103,64 +126,106 @@ export function DataTable<TData, TValue>({
ref={tableContainerRef}
onScroll={onScroll}
className={classNames(
"relative h-[80dvh] overflow-auto", // Set a fixed height for the container
"scrollbar-thin border-subtle relative rounded-md border",
"relative w-full",
"scrollbar-thin h-[80dvh] overflow-auto", // Set a fixed height for the container
"bg-background border-subtle rounded-lg border",
containerClassName
)}
style={{ gridArea: "body" }}>
<TableNew
className="grid border-0"
<Table
className={classNames(
"[&_td]:border-subtle [&_th]:border-subtle border-separate border-spacing-0 [&_tfoot_td]:border-t [&_th]:border-b [&_tr:not(:last-child)_td]:border-b [&_tr]:border-none",
Boolean(enableColumnResizing) && "table-fixed"
)}
style={{
...columnSizingVars,
...(Boolean(enableColumnResizing) && { width: table.getTotalSize() }),
...(Boolean(enableColumnResizing) && { width: table.getCenterTotalSize() }),
}}>
{!hideHeader && (
<TableHeader className="sticky top-0 z-10">
{table.getHeaderGroups().map((headerGroup) => (
<TableRow key={headerGroup.id} className="hover:bg-subtle flex w-full">
<TableRow key={headerGroup.id} className="hover:bg-transparent">
{headerGroup.headers.map((header) => {
const meta = header.column.columnDef.meta;
const { column } = header;
const isPinned = column.getIsPinned();
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinned = isPinned === "right" && column.getIsFirstColumn("right");

return (
<TableHead
key={header.id}
className="bg-muted relative h-10 select-none [&>.cursor-col-resize]:last:opacity-0"
aria-sort={
header.column.getIsSorted() === "asc"
? "ascending"
: header.column.getIsSorted() === "desc"
? "descending"
: "none"
}
data-pinned={isPinned || undefined}
data-last-col={isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined}
style={{
...(meta?.sticky?.position === "left" && { left: `${meta.sticky.gap || 0}px` }),
...(meta?.sticky?.position === "right" && { right: `${meta.sticky.gap || 0}px` }),
...getPinningStyles(column, true),
width: `var(--header-${kebabCase(header?.id)}-size)`,
}}
className={classNames(
"relative flex shrink-0 items-center",
header.column.getCanSort()
? "bg-subtle hover:bg-muted cursor-pointer select-none"
: "",
meta?.sticky && "top-0 z-20 sm:sticky"
)}>
<div
className="flex h-full w-full items-center overflow-hidden"
onClick={header.column.getToggleSortingHandler()}>
{header.isPlaceholder
? null
: flexRender(header.column.columnDef.header, header.getContext())}
{header.column.getIsSorted() && (
<Icon
name="arrow-up"
className="ml-2 h-4 w-4"
style={{
transform:
header.column.getIsSorted() === "asc" ? "rotate(0deg)" : "rotate(180deg)",
transition: "transform 0.2s ease-in-out",
}}>
<div>
{header.isPlaceholder ? null : (
<div
className={classNames(
header.column.getCanSort() &&
"flex h-full cursor-pointer select-none items-center justify-between gap-2"
)}
onClick={header.column.getToggleSortingHandler()}
onKeyDown={(e) => {
// Enhanced keyboard handling for sorting
if (header.column.getCanSort() && (e.key === "Enter" || e.key === " ")) {
e.preventDefault();
header.column.getToggleSortingHandler()?.(e);
}
}}
/>
tabIndex={header.column.getCanSort() ? 0 : undefined}>
<span className="truncate">
{flexRender(header.column.columnDef.header, header.getContext())}
</span>
{{
asc: (
<Icon
name="chevron-up"
size={16}
className="mr-2 shrink-0 opacity-60"
strokeWidth={2}
aria-hidden="true"
/>
),
desc: (
<Icon
name="chevron-down"
size={16}
className="mr-2 shrink-0 opacity-60"
strokeWidth={2}
aria-hidden="true"
/>
),
}[header.column.getIsSorted() as string] ?? null}
</div>
)}
{Boolean(enableColumnResizing) && header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={classNames(
"group absolute right-0 top-0 h-full cursor-col-resize touch-none select-none px-2"
)}>
<div className="bg-subtle group-hover:bg-inverted h-full w-[1px]" />
</div>
)}
</div>
{Boolean(enableColumnResizing) && header.column.getCanResize() && (
<div
onMouseDown={header.getResizeHandler()}
onTouchStart={header.getResizeHandler()}
className={classNames(
"bg-inverted absolute right-0 top-0 h-full w-[5px] cursor-col-resize touch-none select-none opacity-0 hover:opacity-50",
header.column.getIsResizing() && "!opacity-75"
)}
className={classNames(header.column.getIsResizing() && "!opacity-75")}
/>
)}
</TableHead>
Expand Down Expand Up @@ -192,7 +257,7 @@ export function DataTable<TData, TValue>({
onRowMouseclick={onRowMouseclick}
/>
)}
</TableNew>
</Table>
</div>
{children}
</div>
Expand Down Expand Up @@ -232,10 +297,7 @@ function DataTableBody<TData>({
}: DataTableBodyProps<TData>) {
const virtualRows = rowVirtualizer.getVirtualItems();
return (
<TableBody
className="relative grid"
data-testid={testId}
style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
<TableBody data-testid={testId} style={{ height: `${rowVirtualizer.getTotalSize()}px` }}>
{virtualRows && !isPending ? (
virtualRows.map((virtualRow) => {
const row = rows[virtualRow.index] as Row<TData>;
Expand All @@ -245,31 +307,34 @@ function DataTableBody<TData>({
key={row.id}
data-index={virtualRow.index} //needed for dynamic row height measurement
data-state={row.getIsSelected() && "selected"}
onClick={() => onRowMouseclick && onRowMouseclick(row)}
style={{
display: "flex",
position: "absolute",
transform: `translateY(${virtualRow.start}px)`, //this should always be a `style` as it changes on scroll
width: "100%",
}}
className={classNames(onRowMouseclick && "hover:cursor-pointer", "group")}>
className={classNames(
onRowMouseclick && "hover:cursor-pointer",
"has-[[data-state=selected]]:bg-muted/50 group"
)}
onClick={() => onRowMouseclick && onRowMouseclick(row)}>
{row.getVisibleCells().map((cell) => {
const column = table.getColumn(cell.column.id);
const meta = column?.columnDef.meta;
const isPinned = column.getIsPinned();
const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left");
const isFirstRightPinned = isPinned === "right" && column.getIsFirstColumn("right");

return (
<TableCell
key={cell.id}
className="[&[data-pinned]]:bg-default truncate [&[data-pinned]]:backdrop-blur-sm"
data-pinned={isPinned || undefined}
data-last-col={isLastLeftPinned ? "left" : isFirstRightPinned ? "right" : undefined}
style={{
...(meta?.sticky?.position === "left" && { left: `${meta.sticky.gap || 0}px` }),
...(meta?.sticky?.position === "right" && { right: `${meta.sticky.gap || 0}px` }),
...getPinningStyles(column, false),
width: `var(--col-${kebabCase(cell.column.id)}-size)`,
}}
className={classNames(
"flex shrink-0 items-center overflow-hidden",
variant === "compact" && "p-0",
meta?.sticky &&
"bg-default group-hover:!bg-muted group-data-[state=selected]:bg-subtle sm:sticky"
)}>
}}>
{flexRender(cell.column.columnDef.cell, cell.getContext())}
</TableCell>
);
Expand Down
84 changes: 84 additions & 0 deletions packages/features/data-table/components/Table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
import * as React from "react";

import { classNames as cn } from "@calcom/lib";

const Table = React.forwardRef<HTMLTableElement, React.HTMLAttributes<HTMLTableElement>>(
({ className, ...props }, ref) => (
<table ref={ref} className={cn("w-full caption-bottom text-sm", className)} {...props} />
)
);
Table.displayName = "Table";

const TableHeader = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => <thead ref={ref} className={cn(className)} {...props} />
);
TableHeader.displayName = "TableHeader";

const TableBody = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tbody ref={ref} className={cn("grid", "[&_tr:last-child]:border-0", className)} {...props} />
)
);
TableBody.displayName = "TableBody";

const TableFooter = React.forwardRef<HTMLTableSectionElement, React.HTMLAttributes<HTMLTableSectionElement>>(
({ className, ...props }, ref) => (
<tfoot
ref={ref}
className={cn("border-border bg-muted/50 border-t font-medium [&>tr]:last:border-b-0", className)}
{...props}
/>
)
);
TableFooter.displayName = "TableFooter";

const TableRow = React.forwardRef<HTMLTableRowElement, React.HTMLAttributes<HTMLTableRowElement>>(
({ className, ...props }, ref) => (
<tr
ref={ref}
className={cn(
"border-border hover:bg-muted/50 data-[state=selected]:bg-muted border-b transition-colors",
className
)}
{...props}
/>
)
);
TableRow.displayName = "TableRow";

const TableHead = React.forwardRef<HTMLTableCellElement, React.ThHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<th
ref={ref}
className={cn(
"text-muted-foreground h-12 px-3 text-left align-middle font-medium [&:has([role=checkbox])]:w-px [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5",
className
)}
{...props}
/>
)
);
TableHead.displayName = "TableHead";

const TableCell = React.forwardRef<HTMLTableCellElement, React.TdHTMLAttributes<HTMLTableCellElement>>(
({ className, ...props }, ref) => (
<td
ref={ref}
className={cn(
"p-3 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-0.5",
className
)}
{...props}
/>
)
);
TableCell.displayName = "TableCell";

const TableCaption = React.forwardRef<HTMLTableCaptionElement, React.HTMLAttributes<HTMLTableCaptionElement>>(
({ className, ...props }, ref) => (
<caption ref={ref} className={cn("text-muted-foreground mt-4 text-sm", className)} {...props} />
)
);
TableCaption.displayName = "TableCaption";

export { Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow };
2 changes: 1 addition & 1 deletion packages/features/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
"@calcom/trpc": "*",
"@calcom/ui": "*",
"@lexical/react": "^0.9.0",
"@tanstack/react-table": "^8.9.3",
"@tanstack/react-table": "^8.20.6",
"@tanstack/react-virtual": "^3.10.9",
"@vercel/functions": "^1.4.0",
"framer-motion": "^10.12.8",
Expand Down
15 changes: 4 additions & 11 deletions packages/features/users/components/UserTable/UserListTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -265,11 +265,6 @@ function UserListTableContent() {
enableSorting: false,
enableResizing: false,
size: 30,
meta: {
sticky: {
position: "left",
},
},
header: ({ table }) => (
<Checkbox
checked={table.getIsAllPageRowsSelected()}
Expand All @@ -296,9 +291,6 @@ function UserListTableContent() {
header: () => {
return `Members`;
},
meta: {
sticky: { position: "left", gap: 24 },
},
cell: ({ row }) => {
const { username, email, avatarUrl } = row.original;
return (
Expand Down Expand Up @@ -411,9 +403,6 @@ function UserListTableContent() {
enableSorting: false,
enableResizing: false,
size: 80,
meta: {
sticky: { position: "right" },
},
cell: ({ row }) => {
const user = row.original;
const permissionsRaw = permissions;
Expand Down Expand Up @@ -452,6 +441,10 @@ function UserListTableContent() {
manualPagination: true,
initialState: {
columnVisibility: initalColumnVisibility,
columnPinning: {
left: ["select", "member"],
right: ["actions"],
},
},
defaultColumn: {
size: 150,
Expand Down
2 changes: 1 addition & 1 deletion packages/ui/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -78,7 +78,7 @@
"@storybook/blocks": "^7.6.3",
"@storybook/react": "^7.6.3",
"@tanstack/react-query": "^5.17.15",
"@tanstack/react-table": "^8.9.3",
"@tanstack/react-table": "^8.20.6",
"@wojtekmaj/react-daterange-picker": "^3.3.1",
"class-variance-authority": "^0.4.0",
"cmdk": "^0.2.0",
Expand Down
Loading