From a475b5c6df5dcdcc82aa29f1784fc38f49af9690 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Mon, 14 Sep 2026 22:05:50 +0700 Subject: [PATCH] =?UTF-8?q?chore(ui):=20=D1=83=D0=B4=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=81=D1=82=D0=B0=D1=80=D1=83=D1=8E=20=D0=BE?= =?UTF-8?q?=D1=81=D0=BD=D0=BE=D0=B2=D1=83=20Data=20Grid=20=D0=B8=20TanStac?= =?UTF-8?q?k=20v8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../shared/legacy-data-grid-layout.ts | 34 - .../shared/legacy-data-grid-shell.tsx | 69 - .../shared/legacy-data-grid-sort-header.tsx | 51 - components/data-page-card.tsx | 2 +- components/data-page-toolbar.tsx | 2 +- components/data-table.tsx | 26 +- .../data-grid-column-filter.tsx | 167 -- .../data-grid-column-header.tsx | 345 ---- .../data-grid-column-visibility.tsx | 53 - .../legacy-data-grid/data-grid-pagination.tsx | 226 --- .../data-grid-scroll-area.tsx | 421 ----- .../data-grid-table-dnd-rows.tsx | 309 ---- .../legacy-data-grid/data-grid-table-dnd.tsx | 314 ---- .../data-grid-table-virtual.tsx | 492 ------ .../reui/legacy-data-grid/data-grid-table.tsx | 1433 ----------------- .../reui/legacy-data-grid/data-grid.tsx | 270 ---- components/reui/legacy-data-grid/index.ts | 36 - docs/ux-ui-progress.md | 8 + package-lock.json | 35 - package.json | 1 - 20 files changed, 22 insertions(+), 4272 deletions(-) delete mode 100644 components/data-grids/shared/legacy-data-grid-layout.ts delete mode 100644 components/data-grids/shared/legacy-data-grid-shell.tsx delete mode 100644 components/data-grids/shared/legacy-data-grid-sort-header.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-column-filter.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-column-header.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-column-visibility.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-pagination.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-scroll-area.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-table-dnd-rows.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-table-dnd.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-table-virtual.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid-table.tsx delete mode 100644 components/reui/legacy-data-grid/data-grid.tsx delete mode 100644 components/reui/legacy-data-grid/index.ts diff --git a/components/data-grids/shared/legacy-data-grid-layout.ts b/components/data-grids/shared/legacy-data-grid-layout.ts deleted file mode 100644 index 39c88e77..00000000 --- a/components/data-grids/shared/legacy-data-grid-layout.ts +++ /dev/null @@ -1,34 +0,0 @@ -import type { DataGridProps } from "@/components/reui/legacy-data-grid/data-grid" - -const DATA_GRID_CELL_PAD = "py-3" -const DATA_GRID_CELL_PAD_FIRST = "pl-5 py-3" -const DATA_GRID_CELL_PAD_LAST = "pr-4 py-3" - -const DEFAULT_DATA_GRID_LAYOUT: NonNullable["tableLayout"]> = { - rowBorder: true, - headerBackground: true, - headerBorder: true, - columnsResizable: false, -} - -const DEFAULT_DATA_GRID_CLASS_NAMES: NonNullable["tableClassNames"]> = { - headerRow: "border-b border-border", - bodyRow: "group/row", -} - -const DATA_GRID_CONTAINER_CLASS = "w-full rounded-none border-0" - -const DATA_PAGE_CARD_CLASS = "flex w-full flex-col overflow-hidden py-0 gap-0" - -const DATA_PAGE_TOOLBAR_CLASS = "flex items-center gap-3 px-5 py-3 border-b flex-wrap" - -export { - DATA_GRID_CELL_PAD, - DATA_GRID_CELL_PAD_FIRST, - DATA_GRID_CELL_PAD_LAST, - DEFAULT_DATA_GRID_LAYOUT, - DEFAULT_DATA_GRID_CLASS_NAMES, - DATA_GRID_CONTAINER_CLASS, - DATA_PAGE_CARD_CLASS, - DATA_PAGE_TOOLBAR_CLASS, -} diff --git a/components/data-grids/shared/legacy-data-grid-shell.tsx b/components/data-grids/shared/legacy-data-grid-shell.tsx deleted file mode 100644 index c65d0200..00000000 --- a/components/data-grids/shared/legacy-data-grid-shell.tsx +++ /dev/null @@ -1,69 +0,0 @@ -"use client" - -import type { ReactNode } from "react" -import type { Table } from "@tanstack/react-table-v8" -import { - DataGrid, - DataGridContainer, - DataGridPagination, - DataGridTable, - type DataGridProps, -} from "@/components/reui/legacy-data-grid" -import { - DATA_GRID_CONTAINER_CLASS, - DEFAULT_DATA_GRID_CLASS_NAMES, - DEFAULT_DATA_GRID_LAYOUT, -} from "@/components/data-grids/shared/legacy-data-grid-layout" - -interface DataGridShellProps { - table: Table - recordCount: number - children?: ReactNode - pagination?: boolean - isLoading?: boolean - loadingMode?: DataGridProps["loadingMode"] - emptyMessage?: ReactNode - onRowClick?: DataGridProps["onRowClick"] - tableLayout?: DataGridProps["tableLayout"] - tableClassNames?: DataGridProps["tableClassNames"] -} - -function DataGridShell({ - table, - recordCount, - children, - pagination = false, - isLoading, - loadingMode, - emptyMessage, - onRowClick, - tableLayout = DEFAULT_DATA_GRID_LAYOUT, - tableClassNames = DEFAULT_DATA_GRID_CLASS_NAMES, -}: DataGridShellProps) { - return ( - - {children ?? ( - <> - - - - {pagination && recordCount > 0 && ( - - )} - - )} - - ) -} - -export { DataGridShell, type DataGridShellProps } diff --git a/components/data-grids/shared/legacy-data-grid-sort-header.tsx b/components/data-grids/shared/legacy-data-grid-sort-header.tsx deleted file mode 100644 index 4ae800da..00000000 --- a/components/data-grids/shared/legacy-data-grid-sort-header.tsx +++ /dev/null @@ -1,51 +0,0 @@ -"use client" - -import type { Column } from "@tanstack/react-table-v8" -import { cn } from "@/lib/utils" -import { ArrowDownIcon, ArrowUpDownIcon, ArrowUpIcon } from "lucide-react" - -interface DataGridSortHeaderProps { - column: Column - title: string - className?: string -} - -function DataGridSortHeader({ - column, - title, - className, -}: DataGridSortHeaderProps) { - const sorted = column.getIsSorted() - const canSort = column.getCanSort() - - if (!canSort) { - return ( - - {title} - - ) - } - - return ( - - ) -} - -export { DataGridSortHeader, type DataGridSortHeaderProps } diff --git a/components/data-page-card.tsx b/components/data-page-card.tsx index ce42ef83..6bf3d7ff 100644 --- a/components/data-page-card.tsx +++ b/components/data-page-card.tsx @@ -1,6 +1,6 @@ import type { ReactNode } from "react" import { Frame, FramePanel } from "@/components/reui/frame" -import { DATA_PAGE_CARD_CLASS } from "@/components/data-grids/shared/legacy-data-grid-layout" +import { DATA_PAGE_CARD_CLASS } from "@/components/data-grids/shared/data-grid-layout" import { cn } from "@/lib/utils" interface DataPageCardProps { diff --git a/components/data-page-toolbar.tsx b/components/data-page-toolbar.tsx index b75de9f5..0b589e13 100644 --- a/components/data-page-toolbar.tsx +++ b/components/data-page-toolbar.tsx @@ -15,7 +15,7 @@ import { type FilterFieldConfig, } from "@/components/reui/filters" import { SegmentedControl } from "@/components/form-kit" -import { DATA_PAGE_TOOLBAR_CLASS } from "@/components/data-grids/shared/legacy-data-grid-layout" +import { DATA_PAGE_TOOLBAR_CLASS } from "@/components/data-grids/shared/data-grid-layout" function DataPageToolbarFrame({ children, diff --git a/components/data-table.tsx b/components/data-table.tsx index 2ed63ab9..5e75d6e7 100644 --- a/components/data-table.tsx +++ b/components/data-table.tsx @@ -1,20 +1,19 @@ "use client" +import { dataGridFeatures, type DataGridFeatures } from "@/components/reui/data-grid" + import { useMemo, useState } from "react" import { type ColumnDef, - getCoreRowModel, - getPaginationRowModel, - getSortedRowModel, - useReactTable, -} from "@tanstack/react-table-v8" -import { DataGridShell } from "@/components/data-grids/shared/legacy-data-grid-shell" + useTable, +} from "@tanstack/react-table" +import { DataGridShell } from "@/components/data-grids/shared/data-grid-shell" import { DATA_GRID_CELL_PAD, DATA_GRID_CELL_PAD_FIRST, DATA_GRID_CELL_PAD_LAST, -} from "@/components/data-grids/shared/legacy-data-grid-layout" -import { DataGridSortHeader } from "@/components/data-grids/shared/legacy-data-grid-sort-header" +} from "@/components/data-grids/shared/data-grid-layout" +import { DataGridSortHeader } from "@/components/data-grids/shared/data-grid-sort-header" import { DataPageCard } from "@/components/data-page-card" import { DataPageToolbar } from "@/components/data-page-toolbar" import { EmptyState } from "@/components/empty-state" @@ -60,9 +59,9 @@ export function DataTable({ ) }, [data, search, searchKeys]) - const columnDefs = useMemo[]>( + const columnDefs = useMemo[]>( () => { - const defs: ColumnDef[] = columns.map((col, index) => ({ + const defs: ColumnDef[] = columns.map((col, index) => ({ id: col.key, accessorKey: col.key, header: ({ column }) => ( @@ -91,12 +90,11 @@ export function DataTable({ [columns], ) - const table = useReactTable({ + const table = useTable({ + features: dataGridFeatures, + manualPagination: !pagination, data: filteredData, columns: columnDefs, - getCoreRowModel: getCoreRowModel(), - getSortedRowModel: getSortedRowModel(), - ...(pagination ? { getPaginationRowModel: getPaginationRowModel() } : {}), getRowId: (row) => row.id, }) diff --git a/components/reui/legacy-data-grid/data-grid-column-filter.tsx b/components/reui/legacy-data-grid/data-grid-column-filter.tsx deleted file mode 100644 index a0e1fbd2..00000000 --- a/components/reui/legacy-data-grid/data-grid-column-filter.tsx +++ /dev/null @@ -1,167 +0,0 @@ -"use client" - -import { useMemo, useState } from "react" -import { Badge } from "@/components/reui/badge" -import { Column } from "@tanstack/react-table-v8" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Separator } from "@/components/ui/separator" -import { CirclePlusIcon, CheckIcon } from "lucide-react" - -interface DataGridColumnFilterProps { - column?: Column - title?: string - options: { - label: string - value: string - icon?: React.ComponentType<{ className?: string }> - }[] -} - -function DataGridColumnFilter({ - column, - title, - options, -}: DataGridColumnFilterProps) { - const facets = column?.getFacetedUniqueValues() - const selectedValues = new Set(column?.getFilterValue() as string[]) - const [searchQuery, setSearchQuery] = useState("") - - const filteredOptions = useMemo(() => { - if (!searchQuery) return options - return options.filter((option) => - option.label.toLowerCase().includes(searchQuery.toLowerCase()) - ) - }, [options, searchQuery]) - - return ( - - - - {title} - {selectedValues?.size > 0 && ( - <> - - - {selectedValues.size} - -
- {selectedValues.size > 2 ? ( - - {selectedValues.size} selected - - ) : ( - options - .filter((option) => selectedValues.has(option.value)) - .map((option) => ( - - {option.label} - - )) - )} -
- - )} - - } - /> - -
- setSearchQuery(e.target.value)} - className="h-8" - /> -
-
- {filteredOptions.length === 0 ? ( -
- No results found. -
- ) : ( -
- {filteredOptions.map((option) => { - const isSelected = selectedValues.has(option.value) - return ( -
{ - if (isSelected) { - selectedValues.delete(option.value) - } else { - selectedValues.add(option.value) - } - const filterValues = Array.from(selectedValues) - column?.setFilterValue( - filterValues.length ? filterValues : undefined - ) - }} - className={cn( - "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none", - "hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground" - )} - > -
- -
- {option.icon && ( - - )} - {option.label} - {facets?.get(option.value) && ( - - {facets.get(option.value)} - - )} -
- ) - })} -
- )} - {selectedValues.size > 0 && ( - <> -
-
-
column?.setFilterValue(undefined)} - className="hover:bg-accent hover:text-accent-foreground focus:bg-accent focus:text-accent-foreground relative flex cursor-default items-center justify-center rounded-sm px-2 py-1.5 text-sm outline-hidden select-none" - > - Clear filters -
-
- - )} -
- - - ) -} - -export { DataGridColumnFilter, type DataGridColumnFilterProps } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-column-header.tsx b/components/reui/legacy-data-grid/data-grid-column-header.tsx deleted file mode 100644 index 745abed0..00000000 --- a/components/reui/legacy-data-grid/data-grid-column-header.tsx +++ /dev/null @@ -1,345 +0,0 @@ -"use client" - -import { HTMLAttributes, memo, ReactNode, useMemo } from "react" -import { - getColumnHeaderLabel, - useDataGrid, -} from "@/components/reui/legacy-data-grid/data-grid" -import { Column } from "@tanstack/react-table-v8" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuItem, - DropdownMenuLabel, - DropdownMenuSeparator, - DropdownMenuSub, - DropdownMenuSubContent, - DropdownMenuSubTrigger, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react" - -interface DataGridColumnHeaderProps< - TData, - TValue, -> extends HTMLAttributes { - column: Column - /** When omitted, uses `column.columnDef.meta.headerTitle`, then a string `columnDef.header`, then `column.id`. */ - title?: string - icon?: ReactNode - pinnable?: boolean - filter?: ReactNode - visibility?: boolean -} - -function DataGridColumnHeaderInner({ - column, - title, - icon, - className, - filter, - visibility = false, -}: DataGridColumnHeaderProps) { - const { isLoading, table, props, recordCount } = useDataGrid() - const resolvedTitle = title ?? getColumnHeaderLabel(column) - - const columnOrder = table.getState().columnOrder - const columnVisibilityKey = JSON.stringify(table.getState().columnVisibility) - const isSorted = column.getIsSorted() - const isPinned = column.getIsPinned() - const canSort = column.getCanSort() - const canPin = column.getCanPin() - const canResize = column.getCanResize() - - const columnIndex = columnOrder.indexOf(column.id) - const canMoveLeft = columnIndex > 0 - const canMoveRight = columnIndex < columnOrder.length - 1 - - const handleSort = () => { - if (isSorted === "asc") { - column.toggleSorting(true) - } else if (isSorted === "desc") { - column.clearSorting() - } else { - column.toggleSorting(false) - } - } - - const headerLabelClassName = cn( - "text-secondary-foreground/80 inline-flex h-full items-center gap-1.5 font-normal [&_svg]:opacity-60 text-[0.8125rem] leading-[calc(1.125/0.8125)] [&_svg]:size-3.5", - className - ) - - const headerButtonClassName = cn( - "text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground -ms-2 px-2 font-normal h-6 rounded-lg", - className - ) - - const sortIcon = - canSort && - (isSorted === "desc" ? ( - - ) : isSorted === "asc" ? ( - - ) : ( - - )) - - const hasControls = - props.tableLayout?.columnsMovable || - (props.tableLayout?.columnsVisibility && visibility) || - (props.tableLayout?.columnsPinnable && canPin) || - filter - - const menuItems = useMemo(() => { - const items: ReactNode[] = [] - let hasPreviousSection = false - - // Filter section - if (filter) { - items.push( - - {filter} - - ) - hasPreviousSection = true - } - - // Sort section - if (canSort) { - if (hasPreviousSection) { - items.push() - } - items.push( - { - if (isSorted === "asc") { - column.clearSorting() - } else { - column.toggleSorting(false) - } - }} - disabled={!canSort} - > - - Asc - {isSorted === "asc" && ( - - )} - , - { - if (isSorted === "desc") { - column.clearSorting() - } else { - column.toggleSorting(true) - } - }} - disabled={!canSort} - > - - Desc - {isSorted === "desc" && ( - - )} - - ) - hasPreviousSection = true - } - - // Pin section - if (props.tableLayout?.columnsPinnable && canPin) { - if (hasPreviousSection) { - items.push() - } - items.push( - column.pin(isPinned === "left" ? false : "left")} - > - , - column.pin(isPinned === "right" ? false : "right")} - > - - ) - hasPreviousSection = true - } - - // Move section - if (props.tableLayout?.columnsMovable) { - if (hasPreviousSection) { - items.push() - } - items.push( - { - if (columnIndex > 0) { - const newOrder = [...columnOrder] - const [movedColumn] = newOrder.splice(columnIndex, 1) - newOrder.splice(columnIndex - 1, 0, movedColumn) - table.setColumnOrder(newOrder) - } - }} - disabled={!canMoveLeft || isPinned !== false} - > - , - { - if (columnIndex < columnOrder.length - 1) { - const newOrder = [...columnOrder] - const [movedColumn] = newOrder.splice(columnIndex, 1) - newOrder.splice(columnIndex + 1, 0, movedColumn) - table.setColumnOrder(newOrder) - } - }} - disabled={!canMoveRight || isPinned !== false} - > - - ) - hasPreviousSection = true - } - - // Visibility section - if (props.tableLayout?.columnsVisibility && visibility) { - if (hasPreviousSection) { - items.push() - } - items.push( - - - - Columns - - - {table - .getAllColumns() - .filter((col) => col.getCanHide()) - .map((col) => ( - event.preventDefault()} - onCheckedChange={(value) => col.toggleVisibility(!!value)} - className="capitalize" - > - {getColumnHeaderLabel(col)} - - ))} - - - ) - } - - return items - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [ - filter, - canSort, - isSorted, - column, - props.tableLayout?.columnsPinnable, - props.tableLayout?.columnsMovable, - props.tableLayout?.columnsVisibility, - canPin, - isPinned, - canMoveLeft, - canMoveRight, - visibility, - table, - columnIndex, - columnOrder, - columnVisibilityKey, // Needed to update checkbox states when visibility changes - ]) - - if (hasControls) { - return ( -
- - - {icon && icon} - {resolvedTitle} - {sortIcon} - - } - /> - - {menuItems} - - - {props.tableLayout?.columnsPinnable && canPin && isPinned && ( - - )} -
- ) - } - - if (canSort || (props.tableLayout?.columnsResizable && canResize)) { - return ( -
- -
- ) - } - - return ( -
- {icon && icon} - {resolvedTitle} -
- ) -} - -const DataGridColumnHeader = memo( - DataGridColumnHeaderInner -) as typeof DataGridColumnHeaderInner - -export { DataGridColumnHeader, type DataGridColumnHeaderProps } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-column-visibility.tsx b/components/reui/legacy-data-grid/data-grid-column-visibility.tsx deleted file mode 100644 index 3ad29240..00000000 --- a/components/reui/legacy-data-grid/data-grid-column-visibility.tsx +++ /dev/null @@ -1,53 +0,0 @@ -"use client" - -import { ReactElement } from "react" -import { getColumnHeaderLabel } from "@/components/reui/legacy-data-grid/data-grid" -import { Table } from "@tanstack/react-table-v8" - -import { - DropdownMenu, - DropdownMenuCheckboxItem, - DropdownMenuContent, - DropdownMenuGroup, - DropdownMenuLabel, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" - -function DataGridColumnVisibility({ - table, - trigger, -}: { - table: Table - trigger: ReactElement> -}) { - return ( - - - - - - Toggle Columns - - {table - .getAllColumns() - .filter((column) => column.getCanHide()) - .map((column) => { - return ( - event.preventDefault()} - onCheckedChange={(value) => column.toggleVisibility(!!value)} - > - {getColumnHeaderLabel(column)} - - ) - })} - - - - ) -} - -export { DataGridColumnVisibility } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-pagination.tsx b/components/reui/legacy-data-grid/data-grid-pagination.tsx deleted file mode 100644 index ff1cc5ef..00000000 --- a/components/reui/legacy-data-grid/data-grid-pagination.tsx +++ /dev/null @@ -1,226 +0,0 @@ -"use client" - -import React, { ReactNode } from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { Skeleton } from "@/components/ui/skeleton" -import { ChevronLeftIcon, ChevronRightIcon } from "lucide-react" - -interface DataGridPaginationProps { - sizes?: number[] - sizesInfo?: string - sizesLabel?: string - sizesDescription?: string - sizesSkeleton?: ReactNode - more?: boolean - moreLimit?: number - info?: string - infoSkeleton?: ReactNode - className?: string - rowsPerPageLabel?: string - previousPageLabel?: string - nextPageLabel?: string - ellipsisText?: string -} - -function DataGridPagination(props: DataGridPaginationProps): React.JSX.Element { - const { table, recordCount, isLoading } = useDataGrid() - - const defaultProps: Partial = { - sizes: [5, 10, 25, 50, 100], - sizesLabel: "Show", - sizesDescription: "per page", - sizesSkeleton: , - moreLimit: 5, - more: false, - info: "{from} - {to} of {count}", - infoSkeleton: , - rowsPerPageLabel: "Rows per page", - previousPageLabel: "Go to previous page", - nextPageLabel: "Go to next page", - ellipsisText: "...", - } - - const mergedProps: DataGridPaginationProps = { ...defaultProps, ...props } - - const btnBaseClasses = "size-7 p-0 text-sm" - const btnArrowClasses = btnBaseClasses + " rtl:transform rtl:rotate-180" - const pageIndex = table.getState().pagination.pageIndex - const pageSize = table.getState().pagination.pageSize - const from = pageIndex * pageSize + 1 - const to = Math.min((pageIndex + 1) * pageSize, recordCount) - const pageCount = table.getPageCount() - - // Replace placeholders in paginationInfo - const paginationInfo = mergedProps?.info - ? mergedProps.info - .replace("{from}", from.toString()) - .replace("{to}", to.toString()) - .replace("{count}", recordCount.toString()) - : `${from} - ${to} of ${recordCount}` - - // Pagination limit logic - const paginationMoreLimit = mergedProps?.moreLimit || 5 - - // Determine the start and end of the pagination group - const currentGroupStart = - Math.floor(pageIndex / paginationMoreLimit) * paginationMoreLimit - const currentGroupEnd = Math.min( - currentGroupStart + paginationMoreLimit, - pageCount - ) - - // Render page buttons based on the current group - const renderPageButtons = () => { - const buttons = [] - for (let i = currentGroupStart; i < currentGroupEnd; i++) { - buttons.push( - - ) - } - return buttons - } - - // Render a "previous" ellipsis button if there are previous pages to show - const renderEllipsisPrevButton = () => { - if (currentGroupStart > 0) { - return ( - - ) - } - return null - } - - // Render a "next" ellipsis button if there are more pages to show after the current group - const renderEllipsisNextButton = () => { - if (currentGroupEnd < pageCount) { - return ( - - ) - } - return null - } - - return ( -
-
- {isLoading ? ( - mergedProps?.sizesSkeleton - ) : ( - <> -
- {mergedProps.rowsPerPageLabel} -
- - - )} -
-
- {isLoading ? ( - mergedProps?.infoSkeleton - ) : ( - <> -
- {paginationInfo} -
- {pageCount > 1 && ( -
- - - {renderEllipsisPrevButton()} - - {renderPageButtons()} - - {renderEllipsisNextButton()} - - -
- )} - - )} -
-
- ) -} - -export { DataGridPagination, type DataGridPaginationProps } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-scroll-area.tsx b/components/reui/legacy-data-grid/data-grid-scroll-area.tsx deleted file mode 100644 index d846e645..00000000 --- a/components/reui/legacy-data-grid/data-grid-scroll-area.tsx +++ /dev/null @@ -1,421 +0,0 @@ -"use client" - -import { - PointerEvent, - ReactNode, - useCallback, - useEffect, - useRef, - useState, -} from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" -import { ScrollArea as ScrollAreaPrimitive } from "@base-ui/react/scroll-area" - -import { cn } from "@/lib/utils" - -const MIN_THUMB_SIZE = 24 -const FALLBACK_SCROLLBAR_SIZE = 12 - -const INITIAL_METRICS = { - hasVerticalOverflow: false, - headerHeight: 0, - horizontalScrollbarSize: 0, - thumbHeight: 0, - thumbTop: 0, - trackHeight: 0, -} as const - -type DataGridScrollAreaOrientation = "horizontal" | "vertical" | "both" - -type ScrollbarMetrics = { - hasVerticalOverflow: boolean - headerHeight: number - horizontalScrollbarSize: number - thumbHeight: number - thumbTop: number - trackHeight: number -} - -type ObservedElements = { - header: HTMLElement | null - horizontalScrollbar: HTMLElement | null - table: HTMLElement | null - tableViewport: HTMLElement | null -} - -type DataGridScrollAreaProps = Omit< - ScrollAreaPrimitive.Root.Props, - "children" -> & { - children: ReactNode - orientation?: DataGridScrollAreaOrientation -} - -function clamp(value: number, min: number, max: number) { - return Math.min(max, Math.max(min, value)) -} - -function areMetricsEqual(next: ScrollbarMetrics, prev: ScrollbarMetrics) { - return ( - next.hasVerticalOverflow === prev.hasVerticalOverflow && - next.headerHeight === prev.headerHeight && - next.horizontalScrollbarSize === prev.horizontalScrollbarSize && - next.thumbHeight === prev.thumbHeight && - next.thumbTop === prev.thumbTop && - next.trackHeight === prev.trackHeight - ) -} - -function applyMetrics(element: HTMLElement, metrics: ScrollbarMetrics) { - element.style.setProperty( - "--data-grid-scrollbar-header-height", - `${metrics.headerHeight}px` - ) - element.style.setProperty( - "--data-grid-scrollbar-thumb-height", - `${metrics.thumbHeight}px` - ) - element.style.setProperty( - "--data-grid-scrollbar-thumb-top", - `${metrics.thumbTop}px` - ) - element.style.setProperty( - "--data-grid-scrollbar-track-height", - `${metrics.trackHeight}px` - ) -} - -function DataGridScrollArea({ - children, - className, - orientation = "both", - ...props -}: DataGridScrollAreaProps) { - const { props: dataGridProps } = useDataGrid() - const containerRef = useRef(null) - const viewportRef = useRef(null) - const dragRef = useRef<{ - pointerId: number - startScrollTop: number - startY: number - } | null>(null) - const metricsRef = useRef(INITIAL_METRICS) - const observedElementsRef = useRef({ - header: null, - horizontalScrollbar: null, - table: null, - tableViewport: null, - }) - - const showHorizontal = orientation !== "vertical" - const showVertical = orientation !== "horizontal" - const usesCustomVerticalScrollbar = - showVertical && !!dataGridProps.tableLayout?.headerSticky - const [hasCustomVerticalOverflow, setHasCustomVerticalOverflow] = - useState(false) - - const clearDragState = useCallback(() => { - dragRef.current = null - document.body.style.userSelect = "" - document.body.style.webkitUserSelect = "" - }, []) - - const resetMetrics = useCallback(() => { - const container = containerRef.current - - if (container && !areMetricsEqual(INITIAL_METRICS, metricsRef.current)) { - applyMetrics(container, INITIAL_METRICS) - metricsRef.current = INITIAL_METRICS - } - - setHasCustomVerticalOverflow((prev) => (prev ? false : prev)) - }, []) - - const syncCustomVerticalScrollbar = useCallback(() => { - const container = containerRef.current - const viewport = viewportRef.current - - if (!container || !viewport || !usesCustomVerticalScrollbar) { - resetMetrics() - return - } - - const { header, horizontalScrollbar } = observedElementsRef.current - const headerHeight = header?.getBoundingClientRect().height ?? 0 - const viewportHeight = viewport.clientHeight - const viewportWidth = viewport.clientWidth - const scrollHeight = viewport.scrollHeight - const scrollWidth = viewport.scrollWidth - const hasHorizontalOverflow = - showHorizontal && scrollWidth > viewportWidth + 0.5 - const horizontalScrollbarSize = hasHorizontalOverflow - ? horizontalScrollbar?.offsetHeight || FALLBACK_SCROLLBAR_SIZE - : 0 - const trackHeight = Math.max( - 0, - viewportHeight - headerHeight - horizontalScrollbarSize - ) - const maxScroll = Math.max(0, scrollHeight - viewportHeight) - - let nextMetrics: ScrollbarMetrics - - if (trackHeight === 0 || maxScroll === 0) { - nextMetrics = { - hasVerticalOverflow: false, - headerHeight, - horizontalScrollbarSize, - thumbHeight: trackHeight, - thumbTop: 0, - trackHeight, - } - } else { - const bodyContentHeight = Math.max( - trackHeight, - scrollHeight - headerHeight - ) - const thumbHeight = clamp( - trackHeight * (trackHeight / bodyContentHeight), - MIN_THUMB_SIZE, - trackHeight - ) - const maxThumbTop = Math.max(0, trackHeight - thumbHeight) - const thumbTop = - maxThumbTop > 0 ? (viewport.scrollTop / maxScroll) * maxThumbTop : 0 - - nextMetrics = { - hasVerticalOverflow: true, - headerHeight, - horizontalScrollbarSize, - thumbHeight, - thumbTop, - trackHeight, - } - } - - if (!areMetricsEqual(nextMetrics, metricsRef.current)) { - applyMetrics(container, nextMetrics) - metricsRef.current = nextMetrics - } - - setHasCustomVerticalOverflow((prev) => - prev === nextMetrics.hasVerticalOverflow - ? prev - : nextMetrics.hasVerticalOverflow - ) - }, [resetMetrics, showHorizontal, usesCustomVerticalScrollbar]) - - useEffect(() => { - const container = containerRef.current - const viewport = viewportRef.current - - if (!container || !viewport) return - - if (!usesCustomVerticalScrollbar) { - resetMetrics() - return - } - - observedElementsRef.current = { - header: container.querySelector( - '[data-slot="data-grid-table"] thead' - ) as HTMLElement | null, - horizontalScrollbar: container.querySelector( - '[data-slot="data-grid-scrollbar"][data-orientation="horizontal"]' - ) as HTMLElement | null, - table: container.querySelector( - '[data-slot="data-grid-table"]' - ) as HTMLElement | null, - tableViewport: container.querySelector( - '[data-slot="data-grid-table-viewport"]' - ) as HTMLElement | null, - } - - let frame = 0 - - const scheduleSync = () => { - cancelAnimationFrame(frame) - frame = window.requestAnimationFrame(syncCustomVerticalScrollbar) - } - - scheduleSync() - viewport.addEventListener("scroll", scheduleSync, { passive: true }) - - const observer = - typeof ResizeObserver === "undefined" - ? null - : new ResizeObserver(scheduleSync) - - observer?.observe(viewport) - observedElementsRef.current.header && - observer?.observe(observedElementsRef.current.header) - observedElementsRef.current.table && - observer?.observe(observedElementsRef.current.table) - observedElementsRef.current.tableViewport && - observer?.observe(observedElementsRef.current.tableViewport) - - return () => { - cancelAnimationFrame(frame) - observer?.disconnect() - viewport.removeEventListener("scroll", scheduleSync) - clearDragState() - } - }, [ - clearDragState, - resetMetrics, - syncCustomVerticalScrollbar, - usesCustomVerticalScrollbar, - ]) - - const scrollToThumbOffset = (nextThumbTop: number) => { - const viewport = viewportRef.current - const { thumbHeight, trackHeight } = metricsRef.current - - if (!viewport) return - - const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight) - const maxThumbTop = Math.max(0, trackHeight - thumbHeight) - - if (maxScroll === 0 || maxThumbTop === 0) { - viewport.scrollTop = 0 - return - } - - const ratio = clamp(nextThumbTop, 0, maxThumbTop) / maxThumbTop - viewport.scrollTop = ratio * maxScroll - } - - const handleThumbPointerDown = (event: PointerEvent) => { - const viewport = viewportRef.current - - if (!viewport) return - - event.preventDefault() - event.stopPropagation() - event.currentTarget.setPointerCapture(event.pointerId) - - dragRef.current = { - pointerId: event.pointerId, - startScrollTop: viewport.scrollTop, - startY: event.clientY, - } - - document.body.style.userSelect = "none" - document.body.style.webkitUserSelect = "none" - } - - const handleThumbPointerMove = (event: PointerEvent) => { - const viewport = viewportRef.current - const dragState = dragRef.current - const { thumbHeight, trackHeight } = metricsRef.current - - if (!viewport || !dragState || dragState.pointerId !== event.pointerId) { - return - } - - const maxThumbTop = Math.max(0, trackHeight - thumbHeight) - const maxScroll = Math.max(0, viewport.scrollHeight - viewport.clientHeight) - - if (maxThumbTop === 0 || maxScroll === 0) return - - const deltaY = event.clientY - dragState.startY - const nextScrollTop = - dragState.startScrollTop + (deltaY / maxThumbTop) * maxScroll - - viewport.scrollTop = clamp(nextScrollTop, 0, maxScroll) - } - - const handleThumbPointerUp = (event: PointerEvent) => { - if (dragRef.current?.pointerId !== event.pointerId) return - clearDragState() - } - - const handleTrackPointerDown = (event: PointerEvent) => { - const { thumbHeight } = metricsRef.current - - if (event.target !== event.currentTarget) return - - event.preventDefault() - event.stopPropagation() - - const rect = event.currentTarget.getBoundingClientRect() - const offsetY = event.clientY - rect.top - thumbHeight / 2 - - scrollToThumbOffset(offsetY) - } - - return ( -
- - - - {children} - - - - {showHorizontal && ( - - - - )} - - {showVertical && !usesCustomVerticalScrollbar && ( - - - - )} - - - {usesCustomVerticalScrollbar && hasCustomVerticalOverflow && ( - - ) -} - -export { DataGridScrollArea } -export type { DataGridScrollAreaOrientation, DataGridScrollAreaProps } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-table-dnd-rows.tsx b/components/reui/legacy-data-grid/data-grid-table-dnd-rows.tsx deleted file mode 100644 index ff5a3df7..00000000 --- a/components/reui/legacy-data-grid/data-grid-table-dnd-rows.tsx +++ /dev/null @@ -1,309 +0,0 @@ -"use client" - -import { - createContext, - CSSProperties, - ReactNode, - useContext, - useEffect, - useId, - useMemo, - useRef, - useState, -} from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" -import { - DataGridTableBase, - DataGridTableBody, - DataGridTableBodyRow, - DataGridTableBodyRowCell, - DataGridTableBodyRowSkeleton, - DataGridTableBodyRowSkeletonCell, - DataGridTableEmpty, - DataGridTableFoot, - DataGridTableHead, - DataGridTableHeadRow, - DataGridTableHeadRowCell, - DataGridTableHeadRowCellResize, - DataGridTableRowSpacer, - DataGridTableViewport, -} from "@/components/reui/legacy-data-grid/data-grid-table" -import { - closestCenter, - DndContext, - KeyboardSensor, - MouseSensor, - TouchSensor, - UniqueIdentifier, - useSensor, - useSensors, - type DragEndEvent, - type Modifier, -} from "@dnd-kit/core" -import { restrictToVerticalAxis } from "@dnd-kit/modifiers" -import { - SortableContext, - useSortable, - verticalListSortingStrategy, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { Cell, flexRender, HeaderGroup, Row } from "@tanstack/react-table-v8" - -import { cn } from "@/lib/utils" -import { Button } from "@/components/ui/button" -import { GripHorizontalIcon } from "lucide-react" - -// Context to share sortable listeners from row to handle -type SortableContextValue = ReturnType -const SortableRowContext = createContext | null>(null) - -function DataGridTableDndRowHandle({ className }: { className?: string }) { - const context = useContext(SortableRowContext) - - if (!context) { - // Fallback if context is not available (shouldn't happen in normal usage) - return ( - - ) - } - - return ( - - ) -} - -function DataGridTableDndRow({ row }: { row: Row }) { - const { - transform, - transition, - setNodeRef, - isDragging, - attributes, - listeners, - } = useSortable({ - id: row.id, - }) - - const style: CSSProperties = { - transform: CSS.Transform.toString(transform), - transition: transition, - opacity: isDragging ? 0.8 : 1, - zIndex: isDragging ? 1 : 0, - position: "relative", - cursor: isDragging ? "grabbing" : undefined, - } - - return ( - - - {row.getVisibleCells().map((cell: Cell, colIndex) => { - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) - })} - - - ) -} - -function DataGridTableDndRows({ - handleDragEnd, - dataIds, - footerContent, -}: { - handleDragEnd: (event: DragEndEvent) => void - dataIds: UniqueIdentifier[] - footerContent?: ReactNode -}) { - const { table, isLoading, props } = useDataGrid() - const pagination = table.getState().pagination - const tableContainerRef = useRef(null) - const [isDraggingRow, setIsDraggingRow] = useState(false) - - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - useEffect(() => { - if (!isDraggingRow) return - - const { body, documentElement } = document - const previousBodyCursor = body.style.cursor - const previousDocumentCursor = documentElement.style.cursor - - body.style.cursor = "grabbing" - documentElement.style.cursor = "grabbing" - - return () => { - body.style.cursor = previousBodyCursor - documentElement.style.cursor = previousDocumentCursor - } - }, [isDraggingRow]) - - const modifiers = useMemo(() => { - const restrictToTableContainer: Modifier = ({ - transform, - draggingNodeRect, - }) => { - if (!tableContainerRef.current || !draggingNodeRect) { - return transform - } - - const containerRect = tableContainerRef.current.getBoundingClientRect() - const { x, y } = transform - - const minX = containerRect.left - draggingNodeRect.left - const maxX = containerRect.right - draggingNodeRect.right - const minY = containerRect.top - draggingNodeRect.top - const maxY = containerRect.bottom - draggingNodeRect.bottom - - return { - ...transform, - x: Math.max(minX, Math.min(maxX, x)), - y: Math.max(minY, Math.min(maxY, y)), - } - } - - return [restrictToVerticalAxis, restrictToTableContainer] - }, []) - - return ( - setIsDraggingRow(false)} - onDragEnd={(event) => { - setIsDraggingRow(false) - handleDragEnd(event) - }} - onDragStart={() => setIsDraggingRow(true)} - sensors={sensors} - > - - - - {table - .getHeaderGroups() - .map((headerGroup: HeaderGroup, index) => { - return ( - - {headerGroup.headers.map((header, index) => { - const { column } = header - - return ( - - {header.isPlaceholder ? null : props.tableLayout - ?.columnsResizable && column.getCanResize() ? ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- ) : ( - flexRender( - header.column.columnDef.header, - header.getContext() - ) - )} - {props.tableLayout?.columnsResizable && - column.getCanResize() && ( - - )} -
- ) - })} -
- ) - })} -
- - {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( - - )} - - - {props.loadingMode === "skeleton" && - isLoading && - pagination?.pageSize ? ( - Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( - - {table.getVisibleFlatColumns().map((column, colIndex) => { - return ( - - {column.columnDef.meta?.skeleton} - - ) - })} - - )) - ) : table.getRowModel().rows.length ? ( - - {table.getRowModel().rows.map((row: Row) => { - return - })} - - ) : ( - - )} - - - {footerContent && ( - {footerContent} - )} -
-
-
- ) -} - -export { DataGridTableDndRowHandle, DataGridTableDndRows } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-table-dnd.tsx b/components/reui/legacy-data-grid/data-grid-table-dnd.tsx deleted file mode 100644 index 3b93c37d..00000000 --- a/components/reui/legacy-data-grid/data-grid-table-dnd.tsx +++ /dev/null @@ -1,314 +0,0 @@ -"use client" - -import { - CSSProperties, - Fragment, - ReactNode, - useEffect, - useId, - useRef, - useState, -} from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" -import { - DataGridTableBase, - DataGridTableBody, - DataGridTableBodyRow, - DataGridTableBodyRowCell, - DataGridTableBodyRowExpandded, - DataGridTableBodyRowSkeleton, - DataGridTableBodyRowSkeletonCell, - DataGridTableEmpty, - DataGridTableFoot, - DataGridTableHead, - DataGridTableHeadRow, - DataGridTableHeadRowCell, - DataGridTableHeadRowCellResize, - DataGridTableRowSpacer, - DataGridTableViewport, -} from "@/components/reui/legacy-data-grid/data-grid-table" -import { - closestCenter, - DndContext, - KeyboardSensor, - Modifier, - MouseSensor, - TouchSensor, - useSensor, - useSensors, - type DragEndEvent, -} from "@dnd-kit/core" -import { - horizontalListSortingStrategy, - SortableContext, - useSortable, -} from "@dnd-kit/sortable" -import { CSS } from "@dnd-kit/utilities" -import { - Cell, - flexRender, - Header, - HeaderGroup, - Row, -} from "@tanstack/react-table-v8" - -import { Button } from "@/components/ui/button" -import { GripVerticalIcon } from "lucide-react" - -function DataGridTableDndHeader({ - header, -}: { - header: Header -}) { - const { props } = useDataGrid() - const { column } = header - - // Check if column ordering is enabled for this column - const canOrder = - (column.columnDef as { enableColumnOrdering?: boolean }) - .enableColumnOrdering !== false - - const { - attributes, - isDragging, - listeners, - setNodeRef, - transform, - transition, - } = useSortable({ - id: header.column.id, - }) - - const style: CSSProperties = { - opacity: isDragging ? 0.8 : 1, - position: "relative", - transform: CSS.Translate.toString(transform), - transition, - cursor: isDragging ? "grabbing" : undefined, - whiteSpace: "nowrap", - width: props.tableLayout?.columnsResizable - ? `calc(var(--header-${header.id}-size) * 1px)` - : header.column.getSize(), - zIndex: isDragging ? 1 : 0, - } - - return ( - -
- {canOrder && ( - - )} - - {header.isPlaceholder - ? null - : flexRender(header.column.columnDef.header, header.getContext())} - - {props.tableLayout?.columnsResizable && column.getCanResize() && ( - - )} -
-
- ) -} - -function DataGridTableDndCell({ cell }: { cell: Cell }) { - const { props } = useDataGrid() - const { isDragging, setNodeRef, transform, transition } = useSortable({ - id: cell.column.id, - }) - - const style: CSSProperties = { - opacity: isDragging ? 0.8 : 1, - position: "relative", - transform: CSS.Translate.toString(transform), - transition, - cursor: isDragging ? "grabbing" : undefined, - width: props.tableLayout?.columnsResizable - ? `calc(var(--col-${cell.column.id}-size) * 1px)` - : cell.column.getSize(), - zIndex: isDragging ? 1 : 0, - } - - return ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ) -} - -function DataGridTableDnd({ - handleDragEnd, - footerContent, -}: { - handleDragEnd: (event: DragEndEvent) => void - footerContent?: ReactNode -}) { - const { table, isLoading, props } = useDataGrid() - const pagination = table.getState().pagination - const containerRef = useRef(null) - const [isDraggingColumn, setIsDraggingColumn] = useState(false) - - const sensors = useSensors( - useSensor(MouseSensor, {}), - useSensor(TouchSensor, {}), - useSensor(KeyboardSensor, {}) - ) - - useEffect(() => { - if (!isDraggingColumn) return - - const { body, documentElement } = document - const previousBodyCursor = body.style.cursor - const previousDocumentCursor = documentElement.style.cursor - - body.style.cursor = "grabbing" - documentElement.style.cursor = "grabbing" - - return () => { - body.style.cursor = previousBodyCursor - documentElement.style.cursor = previousDocumentCursor - } - }, [isDraggingColumn]) - - // Custom modifier to restrict dragging within table bounds with edge offset - const restrictToTableBounds: Modifier = ({ draggingNodeRect, transform }) => { - if (!draggingNodeRect || !containerRef.current) { - return { ...transform, y: 0 } - } - - const containerRect = containerRef.current.getBoundingClientRect() - const edgeOffset = 0 - - const minX = containerRect.left - draggingNodeRect.left - edgeOffset - const maxX = - containerRect.right - - draggingNodeRect.left - - draggingNodeRect.width + - edgeOffset - - return { - ...transform, - x: Math.min(Math.max(transform.x, minX), maxX), - y: 0, // Lock vertical movement - } - } - - return ( - setIsDraggingColumn(false)} - onDragEnd={(event) => { - setIsDraggingColumn(false) - handleDragEnd(event) - }} - onDragStart={() => setIsDraggingColumn(true)} - sensors={sensors} - > - - - - {table - .getHeaderGroups() - .map((headerGroup: HeaderGroup, index) => { - return ( - - - {headerGroup.headers.map((header) => ( - - ))} - - - ) - })} - - - {(props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( - - )} - - - {props.loadingMode === "skeleton" && - isLoading && - pagination?.pageSize ? ( - Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( - - {table.getVisibleFlatColumns().map((column, colIndex) => { - return ( - - {column.columnDef.meta?.skeleton} - - ) - })} - - )) - ) : table.getRowModel().rows.length ? ( - table.getRowModel().rows.map((row: Row) => { - return ( - - - {row - .getVisibleCells() - .map((cell: Cell) => { - return ( - - - - ) - })} - - {row.getIsExpanded() && ( - - )} - - ) - }) - ) : ( - - )} - - - {footerContent && ( - {footerContent} - )} - - - - ) -} - -export { DataGridTableDnd } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-table-virtual.tsx b/components/reui/legacy-data-grid/data-grid-table-virtual.tsx deleted file mode 100644 index 57cfd6ea..00000000 --- a/components/reui/legacy-data-grid/data-grid-table-virtual.tsx +++ /dev/null @@ -1,492 +0,0 @@ -"use client" - -import { - memo, - ReactNode, - useCallback, - useEffect, - useMemo, - useState, -} from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" -import { - DataGridTableBase, - DataGridTableBody, - DataGridTableEmpty, - DataGridTableFoot, - DataGridTableHead, - DataGridTableHeadRow, - DataGridTableHeadRowCell, - DataGridTableHeadRowCellResize, - DataGridTableRenderedRow, - DataGridTableRowSpacer, - DataGridTableViewport, - getDataGridTableRowSections, -} from "@/components/reui/legacy-data-grid/data-grid-table" -import { flexRender, HeaderGroup, Row, Table } from "@tanstack/react-table-v8" -import { - useVirtualizer, - VirtualItem, - Virtualizer, - VirtualizerOptions, -} from "@tanstack/react-virtual" - -import { cn } from "@/lib/utils" -import { Spinner } from "@/components/ui/spinner" - -type DataGridTableVirtualScrollElements = { - containerElement: HTMLDivElement | null - scrollElement: HTMLElement | null -} - -type DataGridTableVirtualizerInstance = Virtualizer< - HTMLElement, - HTMLTableRowElement -> - -type DataGridTableVirtualizerOptions = Omit< - VirtualizerOptions, - "count" | "estimateSize" | "getItemKey" | "getScrollElement" -> & { - estimateSize?: (index: number, row: Row) => number - getItemKey?: (index: number, row: Row) => string | number - getScrollElement?: ( - elements: DataGridTableVirtualScrollElements - ) => HTMLElement | null -} - -interface DataGridTableVirtualProps { - height?: number | string - estimateSize?: number - overscan?: number - footerContent?: ReactNode - renderHeader?: boolean - onFetchMore?: () => void - isFetchingMore?: boolean - hasMore?: boolean - fetchMoreOffset?: number - virtualizerOptions?: DataGridTableVirtualizerOptions -} - -interface VirtualBodyProps { - table: Table - columnCount: number - topRows: Row[] - centerRows: Row[] - bottomRows: Row[] - virtualItems: VirtualItem[] - totalSize: number - isVirtualizationEnabled: boolean - isInfiniteMode: boolean - isFetchingMore: boolean - hasMore?: boolean - loadingMoreMessage: ReactNode - allRowsLoadedMessage: ReactNode - measureRowRef?: (element: HTMLTableRowElement | null) => void -} - -function DataGridTableVirtualSpacer({ - columnCount, - height, -}: { - columnCount: number - height: number -}) { - if (height <= 0) return null - - return ( - - - - ) -} - -function DataGridTableVirtualStatusRow({ - children, - className, - columnCount, -}: { - children: ReactNode - className?: string - columnCount: number -}) { - return ( - - - {children} - - - ) -} - -function DataGridTableVirtualBody({ - table, - columnCount, - topRows, - centerRows, - bottomRows, - virtualItems, - totalSize, - isVirtualizationEnabled, - isInfiniteMode, - isFetchingMore, - hasMore, - loadingMoreMessage, - allRowsLoadedMessage, - measureRowRef, -}: VirtualBodyProps) { - const totalRows = topRows.length + centerRows.length + bottomRows.length - - if (!totalRows) return - - const hasCenterRows = centerRows.length > 0 - const showFetchingRow = isInfiniteMode && isFetchingMore - const showCompleteRow = isInfiniteMode && hasMore === false && totalRows > 0 - const hasMiddleSection = hasCenterRows || showFetchingRow || showCompleteRow - const leadingSpacerHeight = - isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 - ? (virtualItems[0]?.start ?? 0) - : 0 - const trailingSpacerHeight = - isVirtualizationEnabled && hasCenterRows && virtualItems.length > 0 - ? Math.max( - 0, - totalSize - (virtualItems[virtualItems.length - 1]?.end ?? 0) - ) - : 0 - - const renderedRows: ReactNode[] = [] - - topRows.forEach((row, index) => { - renderedRows.push( - - ) - }) - - if (isVirtualizationEnabled) { - if (leadingSpacerHeight > 0) { - renderedRows.push( - - ) - } - - virtualItems.forEach((virtualRow) => { - const row = centerRows[virtualRow.index] - - if (!row) return - - renderedRows.push( - - ) - }) - - if (trailingSpacerHeight > 0) { - renderedRows.push( - - ) - } - } else { - centerRows.forEach((row) => { - renderedRows.push() - }) - } - - if (showFetchingRow) { - renderedRows.push( - -
- - {loadingMoreMessage} -
-
- ) - } - - if (showCompleteRow) { - renderedRows.push( - - {allRowsLoadedMessage} - - ) - } - - bottomRows.forEach((row, index) => { - renderedRows.push( - 0 || hasMiddleSection) - ? "bottom" - : undefined - } - /> - ) - }) - - return <>{renderedRows} -} - -/** - * Memoized virtual body: skip re-renders during active column resize. - * Column widths update via CSS variables on the element, - * so the browser handles width changes without React re-renders. - */ -const MemoizedVirtualBody = memo( - DataGridTableVirtualBody, - (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn -) as typeof DataGridTableVirtualBody - -function DataGridTableVirtual({ - height, - estimateSize = 48, - overscan = 10, - footerContent, - renderHeader = true, - onFetchMore, - isFetchingMore = false, - hasMore, - fetchMoreOffset = 0, - virtualizerOptions, -}: DataGridTableVirtualProps) { - const { table, props } = useDataGrid() - const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( - table, - props.tableLayout?.rowsPinnable - ) - const columnCount = - table.getVisibleFlatColumns().length + - (props.tableLayout?.columnsResizable ? 1 : 0) - const isInfiniteMode = typeof onFetchMore === "function" - const [viewportElements, setViewportElements] = - useState({ - containerElement: null, - scrollElement: null, - }) - - const { - estimateSize: customEstimateSize, - getItemKey: customGetItemKey, - getScrollElement: customGetScrollElement, - measureElement: customMeasureElement, - overscan: customOverscan, - ...virtualizerOptionsRest - } = virtualizerOptions ?? {} - - const isVirtualizationEnabled = virtualizerOptions?.enabled !== false - const loadingMoreMessage = - props.fetchingMoreMessage || props.loadingMessage || "Loading..." - const allRowsLoadedMessage = - props.allRowsLoadedMessage || "All records loaded" - - const handleViewportRef = useCallback((node: HTMLDivElement | null) => { - setViewportElements({ - containerElement: node, - scrollElement: - (node?.closest( - '[data-slot="scroll-area-viewport"]' - ) as HTMLElement | null) ?? node, - }) - }, []) - - const usesExternalScrollArea = - viewportElements.scrollElement !== null && - viewportElements.scrollElement !== viewportElements.containerElement - - const resolveScrollElement = useCallback(() => { - if (customGetScrollElement) { - return customGetScrollElement(viewportElements) - } - - return viewportElements.scrollElement - }, [customGetScrollElement, viewportElements]) - - const resolveItemKey = useCallback( - (index: number) => { - const row = centerRows[index] - - if (!row) return index - - return customGetItemKey?.(index, row) ?? row.id ?? index - }, - [centerRows, customGetItemKey] - ) - - const resolveEstimateSize = useCallback( - (index: number) => { - const row = centerRows[index] - - return row - ? (customEstimateSize?.(index, row) ?? estimateSize) - : estimateSize - }, - [centerRows, customEstimateSize, estimateSize] - ) - - const virtualizer = useVirtualizer({ - count: centerRows.length, - getScrollElement: resolveScrollElement, - getItemKey: resolveItemKey, - estimateSize: resolveEstimateSize, - overscan: customOverscan ?? overscan, - measureElement: customMeasureElement, - ...virtualizerOptionsRest, - }) as DataGridTableVirtualizerInstance - - const virtualItems = isVirtualizationEnabled - ? virtualizer.getVirtualItems() - : [] - const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0 - const measureRowRef = - isVirtualizationEnabled && customMeasureElement - ? virtualizer.measureElement - : undefined - const resolvedFetchMoreOffset = useMemo( - () => Math.max(0, fetchMoreOffset), - [fetchMoreOffset] - ) - - useEffect(() => { - if ( - !isVirtualizationEnabled || - !isInfiniteMode || - hasMore === false || - isFetchingMore - ) { - return - } - - const lastItem = virtualItems[virtualItems.length - 1] - if (!lastItem) return - - if (lastItem.index >= centerRows.length - 1 - resolvedFetchMoreOffset) { - onFetchMore?.() - } - }, [ - centerRows.length, - hasMore, - isFetchingMore, - isInfiniteMode, - isVirtualizationEnabled, - onFetchMore, - resolvedFetchMoreOffset, - virtualItems, - ]) - - return ( - - - {renderHeader && ( - - {table - .getHeaderGroups() - .map((headerGroup: HeaderGroup, index) => ( - - {headerGroup.headers.map((header, hIndex) => { - const { column } = header - - return ( - - {header.isPlaceholder ? null : props.tableLayout - ?.columnsResizable && column.getCanResize() ? ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- ) : ( - flexRender( - header.column.columnDef.header, - header.getContext() - ) - )} - {props.tableLayout?.columnsResizable && - column.getCanResize() && ( - - )} -
- ) - })} -
- ))} -
- )} - - {renderHeader && - (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( - - )} - - - - - - {footerContent && ( - {footerContent} - )} -
-
- ) -} - -export { DataGridTableVirtual } -export type { - DataGridTableVirtualProps, - DataGridTableVirtualScrollElements, - DataGridTableVirtualizerOptions, -} \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid-table.tsx b/components/reui/legacy-data-grid/data-grid-table.tsx deleted file mode 100644 index 80894108..00000000 --- a/components/reui/legacy-data-grid/data-grid-table.tsx +++ /dev/null @@ -1,1433 +0,0 @@ -"use client" - -import { - CSSProperties, - Fragment, - memo, - MouseEvent as ReactMouseEvent, - ReactNode, - TouchEvent as ReactTouchEvent, - Ref, - useCallback, - useEffect, - useMemo, - useState, -} from "react" -import { useDataGrid } from "@/components/reui/legacy-data-grid/data-grid" -import { - Cell, - Column, - flexRender, - Header, - HeaderGroup, - Row, - Table, -} from "@tanstack/react-table-v8" -import { cva } from "class-variance-authority" - -import { cn } from "@/lib/utils" -import { Checkbox } from "@/components/ui/checkbox" -import { Spinner } from "@/components/ui/spinner" - -const headerCellSpacingVariants = cva("", { - variants: { - size: { - dense: - "px-2 h-8", - default: - "px-3", - }, - }, - defaultVariants: { - size: "default", - }, -}) - -const bodyCellSpacingVariants = cva("", { - variants: { - size: { - dense: - "px-2 py-1.5", - default: - "px-3 py-2", - }, - }, - defaultVariants: { - size: "default", - }, -}) - -const footerCellSpacingVariants = cva("", { - variants: { - size: { - dense: - "px-2 py-1.5", - default: - "px-3 py-2", - }, - }, - defaultVariants: { - size: "default", - }, -}) - -function getPinningStyles(column: Column): CSSProperties { - const isPinned = column.getIsPinned() - - 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: isPinned ? 1 : 0, - } -} - -function assignRef(ref: Ref | undefined, value: T | null) { - if (!ref) return - - if (typeof ref === "function") { - ref(value) - return - } - - ;(ref as { current: T | null }).current = value -} - -type DataGridResizeStartEvent = - | ReactMouseEvent - | ReactTouchEvent - -type DataGridResizeDocumentEvent = globalThis.MouseEvent | globalThis.TouchEvent - -function isDataGridTouchEvent( - event: DataGridResizeStartEvent | DataGridResizeDocumentEvent -): event is ReactTouchEvent | globalThis.TouchEvent { - return "touches" in event -} - -function getDataGridResizeEventClientX( - event: DataGridResizeStartEvent | DataGridResizeDocumentEvent -) { - if (isDataGridTouchEvent(event)) { - return event.touches[0]?.clientX ?? event.changedTouches[0]?.clientX - } - - return event.clientX -} - -function startDataGridColumnResizeOnEnd( - event: DataGridResizeStartEvent, - header: Header, - table: Table -) { - const column = table.getColumn(header.column.id) - - if (!column || !column.getCanResize()) return - if (isDataGridTouchEvent(event) && event.touches.length > 1) return - - event.persist?.() - - const ownerDocument = event.currentTarget.ownerDocument - const previousBodyCursor = ownerDocument.body.style.cursor - const previousDocumentCursor = ownerDocument.documentElement.style.cursor - const startSize = header.getSize() - const dragStartClientX = getDataGridResizeEventClientX(event) - const headerCell = event.currentTarget.closest("th") - const headerRect = headerCell?.getBoundingClientRect() - const startOffset = - headerRect && - Number.isFinite( - table.options.columnResizeDirection === "rtl" - ? headerRect.left - : headerRect.right - ) - ? table.options.columnResizeDirection === "rtl" - ? headerRect.left - : headerRect.right - : dragStartClientX - - if (typeof dragStartClientX !== "number" || typeof startOffset !== "number") { - return - } - - ownerDocument.body.style.cursor = "col-resize" - ownerDocument.documentElement.style.cursor = "col-resize" - - const columnSizingStart = header - .getLeafHeaders() - .map( - (leafHeader) => - [leafHeader.column.id, leafHeader.column.getSize()] as [string, number] - ) - const directionMultiplier = - table.options.columnResizeDirection === "rtl" ? -1 : 1 - - const updateOffset = (clientXPos?: number, commit = false) => { - if (typeof clientXPos !== "number") return - - let nextColumnSizing: Record = {} - const deltaOffset = (clientXPos - dragStartClientX) * directionMultiplier - const deltaPercentage = Math.max(deltaOffset / startSize, -0.999999) - - columnSizingStart.forEach(([columnId, headerSize]) => { - nextColumnSizing[columnId] = - Math.round( - Math.max(headerSize + headerSize * deltaPercentage, 0) * 100 - ) / 100 - }) - - table.setColumnSizingInfo((old) => ({ - ...old, - startOffset, - startSize, - deltaOffset, - deltaPercentage, - columnSizingStart, - isResizingColumn: column.id, - })) - - if (commit) { - table.setColumnSizing((old) => ({ - ...old, - ...nextColumnSizing, - })) - } - } - - const endResize = (clientXPos?: number) => { - updateOffset(clientXPos, true) - table.setColumnSizingInfo((old) => ({ - ...old, - isResizingColumn: false, - startOffset: null, - startSize: null, - deltaOffset: null, - deltaPercentage: null, - columnSizingStart: [], - })) - ownerDocument.body.style.cursor = previousBodyCursor - ownerDocument.documentElement.style.cursor = previousDocumentCursor - } - - const mouseMoveHandler = (moveEvent: globalThis.MouseEvent) => { - updateOffset(moveEvent.clientX) - } - const mouseUpHandler = (upEvent: globalThis.MouseEvent) => { - ownerDocument.removeEventListener("mousemove", mouseMoveHandler) - ownerDocument.removeEventListener("mouseup", mouseUpHandler) - endResize(upEvent.clientX) - } - const touchMoveHandler = (moveEvent: globalThis.TouchEvent) => { - if (moveEvent.cancelable) { - moveEvent.preventDefault() - moveEvent.stopPropagation() - } - - updateOffset(getDataGridResizeEventClientX(moveEvent)) - } - const touchEndHandler = (endEvent: globalThis.TouchEvent) => { - ownerDocument.removeEventListener("touchmove", touchMoveHandler) - ownerDocument.removeEventListener("touchend", touchEndHandler) - - if (endEvent.cancelable) { - endEvent.preventDefault() - endEvent.stopPropagation() - } - - endResize(getDataGridResizeEventClientX(endEvent)) - } - - const passiveIfSupported = { passive: false } as const - - if (isDataGridTouchEvent(event)) { - ownerDocument.addEventListener( - "touchmove", - touchMoveHandler, - passiveIfSupported - ) - ownerDocument.addEventListener( - "touchend", - touchEndHandler, - passiveIfSupported - ) - } else { - ownerDocument.addEventListener( - "mousemove", - mouseMoveHandler, - passiveIfSupported - ) - ownerDocument.addEventListener( - "mouseup", - mouseUpHandler, - passiveIfSupported - ) - } - - table.setColumnSizingInfo((old) => ({ - ...old, - startOffset, - startSize, - deltaOffset: 0, - deltaPercentage: 0, - columnSizingStart, - isResizingColumn: column.id, - })) -} - -type DataGridTablePinnedBoundary = "top" | "bottom" - -function getDataGridTableRowSections( - table: Table, - rowsPinnable?: boolean -) { - if (!rowsPinnable) { - return { - topRows: [] as Row[], - centerRows: table.getRowModel().rows as Row[], - bottomRows: [] as Row[], - } - } - - return { - topRows: table.getTopRows() as Row[], - centerRows: table.getCenterRows() as Row[], - bottomRows: table.getBottomRows() as Row[], - } -} - -function getDataGridTableResolvedRows( - table: Table, - rowsPinnable?: boolean -) { - const { topRows, centerRows, bottomRows } = getDataGridTableRowSections( - table, - rowsPinnable - ) - const resolvedRows: Array<{ - row: Row - pinnedBoundary?: DataGridTablePinnedBoundary - }> = [] - - topRows.forEach((row, index) => { - resolvedRows.push({ - row, - pinnedBoundary: - index === topRows.length - 1 && - (centerRows.length > 0 || bottomRows.length > 0) - ? "top" - : undefined, - }) - }) - - centerRows.forEach((row) => { - resolvedRows.push({ row }) - }) - - bottomRows.forEach((row, index) => { - resolvedRows.push({ - row, - pinnedBoundary: - index === 0 && (centerRows.length > 0 || topRows.length > 0) - ? "bottom" - : undefined, - }) - }) - - return resolvedRows -} - -function DataGridTableFillCol() { - const { props } = useDataGrid() - - if (!props.tableLayout?.columnsResizable) return null - - return ( -
- ) -} - -function DataGridTableFillHeadCell() { - const { props } = useDataGrid() - - if (!props.tableLayout?.columnsResizable) return null - - return ( - - {children} - - ) -} - -function DataGridTableHeadRow({ - children, - headerGroup, -}: { - children: ReactNode - headerGroup: HeaderGroup -}) { - const { props } = useDataGrid() - - return ( - th]:border-b", - props.tableLayout?.cellBorder && "*:last:border-e-0", - props.tableLayout?.stripped && "bg-transparent", - props.tableLayout?.headerBackground === false && "bg-transparent", - props.tableClassNames?.headerRow - )} - > - {children} - - - ) -} - -function DataGridTableHeadRowCell({ - children, - header, - dndRef, - dndStyle, -}: { - children: ReactNode - header: Header - dndRef?: React.Ref - dndStyle?: CSSProperties -}) { - const { props } = useDataGrid() - - const { column } = header - const isPinned = column.getIsPinned() - const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left") - const isFirstRightPinned = - isPinned === "right" && column.getIsFirstColumn("right") - const isLastVisibleColumn = - column.getIndex() === - header.getContext().table.getVisibleLeafColumns().length - 1 - const headerCellSpacing = headerCellSpacingVariants({ - size: props.tableLayout?.dense ? "dense" : "default", - }) - - return ( - - ) -} - -function DataGridTableHeadRowCellResize({ - header, -}: { - header: Header -}) { - const { props, table } = useDataGrid() - const { column } = header - const isLastVisibleColumn = - column.getIndex() === - header.getContext().table.getVisibleLeafColumns().length - 1 - const isResizeModeOnEnd = - (props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode) === - "onEnd" - - const handleMouseDown = (event: ReactMouseEvent) => { - event.preventDefault() - event.stopPropagation() - - if (isResizeModeOnEnd) { - startDataGridColumnResizeOnEnd(event, header, table) - return - } - - header.getResizeHandler()(event) - } - - const handleTouchStart = (event: ReactTouchEvent) => { - event.preventDefault() - event.stopPropagation() - - if (isResizeModeOnEnd) { - startDataGridColumnResizeOnEnd(event, header, table) - return - } - - header.getResizeHandler()(event) - } - - return ( -
column.resetSize(), - onMouseDown: handleMouseDown, - onTouchStart: handleTouchStart, - className: cn( - "absolute top-0 h-full cursor-col-resize user-select-none touch-none z-10 flex", - isLastVisibleColumn - ? "end-0 w-5 justify-end before:hidden" - : "-end-2 w-5 justify-center before:absolute before:inset-y-0 before:w-px before:-translate-x-px before:bg-border", - column.getIsResizing() && - (isResizeModeOnEnd - ? "opacity-100" - : isLastVisibleColumn - ? "before:absolute before:end-0 before:block before:inset-y-0 before:w-0.5 before:bg-primary opacity-100" - : "before:block before:bg-primary before:w-0.5 opacity-100") - ), - }} - /> - ) -} - -function DataGridTableResizeIndicator({ - viewportElement, -}: { - viewportElement: HTMLDivElement | null -}) { - const { props, table } = useDataGrid() - const columnSizingInfo = table.getState().columnSizingInfo - const resizingColumnId = columnSizingInfo.isResizingColumn - const resizeMode = - props.tableLayout?.columnsResizeMode ?? table.options.columnResizeMode - - if ( - !props.tableLayout?.columnsResizable || - resizeMode !== "onEnd" || - !resizingColumnId - ) { - return null - } - - const resizingHeader = table - .getFlatHeaders() - .find( - (header) => - header.column.id === resizingColumnId || header.id === resizingColumnId - ) - - if (!resizingHeader) return null - - const deltaOffset = columnSizingInfo.deltaOffset ?? 0 - const headerHeight = - viewportElement - ?.querySelector('[data-slot="data-grid-table"] thead') - ?.getBoundingClientRect().height ?? 0 - const indicatorLeft = - typeof columnSizingInfo.startOffset === "number" && viewportElement - ? columnSizingInfo.startOffset - - viewportElement.getBoundingClientRect().left - : resizingHeader.getStart() + resizingHeader.getSize() - - return ( -
-} - -function DataGridTableBody({ children }: { children: ReactNode }) { - const { props } = useDataGrid() - - return ( - - {children} - - ) -} - -function DataGridTableFoot({ children }: { children: ReactNode }) { - const { props } = useDataGrid() - return ( - - {children} - - ) -} - -function DataGridTableFootRow({ children }: { children: ReactNode }) { - const { props } = useDataGrid() - return ( - - {children} - - - ) -} - -function DataGridTableFootRowCell({ - children, - colSpan, - className, -}: { - children?: ReactNode - colSpan?: number - className?: string -}) { - const { props } = useDataGrid() - const spacing = footerCellSpacingVariants({ - size: props.tableLayout?.dense ? "dense" : "default", - }) - return ( - - ) -} - -function DataGridTableBodyRowSkeleton({ children }: { children: ReactNode }) { - const { table, props } = useDataGrid() - - return ( - td]:border-b", - props.tableLayout?.cellBorder && "*:last:border-e-0", - props.tableLayout?.stripped && - "odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent", - table.options.enableRowSelection && "*:first:relative", - props.tableClassNames?.bodyRow - )} - > - {children} - - - ) -} - -function DataGridTableBodyRowSkeletonCell({ - children, - column, -}: { - children: ReactNode - column: Column -}) { - const { props, table } = useDataGrid() - const bodyCellSpacing = bodyCellSpacingVariants({ - size: props.tableLayout?.dense ? "dense" : "default", - }) - - return ( - - ) -} - -function DataGridTableBodyRow({ - children, - row, - pinnedBoundary, - rowRef, - dndRef, - dndStyle, -}: { - children: ReactNode - row: Row - pinnedBoundary?: DataGridTablePinnedBoundary - rowRef?: React.Ref - dndRef?: React.Ref - dndStyle?: CSSProperties -}) { - const { props, table } = useDataGrid() - const isRowPinned = row.getIsPinned() - - return ( - { - assignRef(rowRef, node) - assignRef(dndRef, node) - }} - style={{ ...(dndStyle ? dndStyle : null) }} - data-state={ - table.options.enableRowSelection && row.getIsSelected() - ? "selected" - : undefined - } - data-row-pinned={isRowPinned || undefined} - data-row-pinned-boundary={pinnedBoundary} - onClick={() => props.onRowClick && props.onRowClick(row.original)} - className={cn( - "hover:bg-muted/40 data-[state=selected]:bg-muted/50", - props.onRowClick && "cursor-pointer", - !props.tableLayout?.stripped && - props.tableLayout?.rowBorder && - "border-border border-b [&:not(:last-child)>td]:border-b", - props.tableLayout?.cellBorder && "*:last:border-e-0", - props.tableLayout?.stripped && - "odd:bg-muted/90 odd:hover:bg-muted hover:bg-transparent", - table.options.enableRowSelection && "*:first:relative", - props.tableLayout?.rowsPinnable && - isRowPinned && - "bg-muted/30 hover:bg-muted/50", - pinnedBoundary === "top" && "[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]", - pinnedBoundary === "bottom" && - "[&>td]:shadow-[0_2px_0_rgba(0,0,0,0.03)]", - props.tableClassNames?.bodyRow - )} - > - {children} - - - ) -} - -function DataGridTableBodyRowExpandded({ row }: { row: Row }) { - const { props, table } = useDataGrid() - - return ( - td]:border-b" - )} - > - - - ) -} - -function DataGridTableBodyRowCell({ - children, - cell, - dndRef, - dndStyle, -}: { - children: ReactNode - cell: Cell - dndRef?: React.Ref - dndStyle?: CSSProperties -}) { - const { props } = useDataGrid() - - const { column, row } = cell - const isPinned = column.getIsPinned() - const isLastLeftPinned = isPinned === "left" && column.getIsLastColumn("left") - const isFirstRightPinned = - isPinned === "right" && column.getIsFirstColumn("right") - const bodyCellSpacing = bodyCellSpacingVariants({ - size: props.tableLayout?.dense ? "dense" : "default", - }) - - return ( - - ) -} - -function DataGridTableRenderedRow({ - row, - pinnedBoundary, - rowRef, -}: { - row: Row - pinnedBoundary?: DataGridTablePinnedBoundary - rowRef?: React.Ref -}) { - return ( - - - {row.getVisibleCells().map((cell: Cell) => ( - - {flexRender(cell.column.columnDef.cell, cell.getContext())} - - ))} - - {row.getIsExpanded() && } - - ) -} - -function DataGridTableEmpty() { - const { table, props } = useDataGrid() - const visibleColumnCount = - table.getVisibleLeafColumns().length + - (props.tableLayout?.columnsResizable ? 1 : 0) - - return ( - - - - ) -} - -function DataGridTableLoader() { - const { props } = useDataGrid() - - return ( -
-
- - {props.loadingMessage || "Loading..."} -
-
- ) -} - -function DataGridTableRowPin({ row }: { row: Row }) { - const isPinned = row.getIsPinned() - - return ( - - ) -} - -function DataGridTableRowSelect({ row }: { row: Row }) { - return ( - <> - - row.toggleSelected(!!value)} - aria-label="Select row" - className="align-[inherit]" - /> - - ) -} - -function DataGridTableRowSelectAll() { - const { table, recordCount, isLoading } = useDataGrid() - - const isAllSelected = table.getIsAllPageRowsSelected() - const isSomeSelected = table.getIsSomePageRowsSelected() - - return ( - table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="align-[inherit]" - /> - ) -} - -function DataGridTableBodyRows({ table }: { table: Table }) { - const { isLoading, props } = useDataGrid() - const pagination = table.getState().pagination - - if (isLoading && props.loadingMode === "skeleton" && pagination?.pageSize) { - return ( - <> - {Array.from({ length: pagination.pageSize }).map((_, rowIndex) => ( - - {table.getVisibleFlatColumns().map((column, colIndex) => ( - - {column.columnDef.meta?.skeleton} - - ))} - - ))} - - ) - } - - if (isLoading && props.loadingMode === "spinner") { - return ( - - - - ) - } - - const resolvedRows = getDataGridTableResolvedRows( - table, - props.tableLayout?.rowsPinnable - ) - - if (!resolvedRows.length) return - - return ( - <> - {resolvedRows.map(({ row, pinnedBoundary }) => ( - - ))} - - ) -} - -/** - * Memoized body rows: skip re-renders during active column resize. - * Column widths update via CSS variables on the
- {children} -
- {children} -
- {children} -
- {table - .getAllColumns() - .find((column) => column.columnDef.meta?.expandedContent) - ?.columnDef.meta?.expandedContent?.(row.original)} -
- {children} -
- {props.emptyMessage || "No data available"} -
-
- - - - - {props.loadingMessage || "Loading..."} -
-
element, - * so the browser handles width changes without React re-renders. - */ -const MemoizedDataGridTableBodyRows = memo( - DataGridTableBodyRows, - (_prev, next) => !!next.table.getState().columnSizingInfo.isResizingColumn -) as typeof DataGridTableBodyRows - -function DataGridTableHeader() { - const { table, props } = useDataGrid() - - return ( - - - - {table - .getHeaderGroups() - .map((headerGroup: HeaderGroup, index) => { - return ( - - {headerGroup.headers.map((header, index) => { - const { column } = header - - return ( - - {header.isPlaceholder ? null : props.tableLayout - ?.columnsResizable && column.getCanResize() ? ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- ) : ( - flexRender( - header.column.columnDef.header, - header.getContext() - ) - )} - {props.tableLayout?.columnsResizable && - column.getCanResize() && ( - - )} -
- ) - })} -
- ) - })} -
-
-
- ) -} - -function DataGridTable({ - footerContent, - renderHeader = true, -}: { - footerContent?: ReactNode - renderHeader?: boolean -}) { - const { table, props } = useDataGrid() - - return ( - - - {renderHeader && ( - - {table - .getHeaderGroups() - .map((headerGroup: HeaderGroup, index) => { - return ( - - {headerGroup.headers.map((header, index) => { - const { column } = header - - return ( - - {header.isPlaceholder ? null : props.tableLayout - ?.columnsResizable && column.getCanResize() ? ( -
- {flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- ) : ( - flexRender( - header.column.columnDef.header, - header.getContext() - ) - )} - {props.tableLayout?.columnsResizable && - column.getCanResize() && ( - - )} -
- ) - })} -
- ) - })} -
- )} - - {renderHeader && - (props.tableLayout?.stripped || !props.tableLayout?.rowBorder) && ( - - )} - - - - - - {footerContent && ( - {footerContent} - )} -
-
- ) -} - -export { - DataGridTable, - DataGridTableBase, - DataGridTableBody, - DataGridTableBodyRow, - DataGridTableBodyRowCell, - DataGridTableBodyRowExpandded, - DataGridTableRenderedRow, - DataGridTableBodyRowSkeleton, - DataGridTableBodyRowSkeletonCell, - DataGridTableEmpty, - DataGridTableFoot, - DataGridTableFootRow, - DataGridTableFootRowCell, - DataGridTableHeader, - DataGridTableHead, - DataGridTableHeadRow, - DataGridTableHeadRowCell, - DataGridTableHeadRowCellResize, - DataGridTableLoader, - DataGridTableRowPin, - DataGridTableRowSelect, - DataGridTableRowSelectAll, - DataGridTableRowSpacer, - DataGridTableViewport, - getDataGridTableResolvedRows, - getDataGridTableRowSections, -} - -export type { DataGridTablePinnedBoundary } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/data-grid.tsx b/components/reui/legacy-data-grid/data-grid.tsx deleted file mode 100644 index a4624a8a..00000000 --- a/components/reui/legacy-data-grid/data-grid.tsx +++ /dev/null @@ -1,270 +0,0 @@ -"use client" - -import { createContext, ReactNode, useContext, useMemo } from "react" -import { - Column, - ColumnFiltersState, - RowData, - SortingState, - Table, -} from "@tanstack/react-table-v8" - -import { cn } from "@/lib/utils" - -declare module "@tanstack/react-table-v8" { - // eslint-disable-next-line @typescript-eslint/no-unused-vars - interface ColumnMeta { - headerTitle?: string - headerClassName?: string - cellClassName?: string - skeleton?: ReactNode - expandedContent?: (row: TData) => ReactNode - } -} - -/** Label for headers / column visibility: `meta.headerTitle`, string `columnDef.header`, or `column.id`. */ -export function getColumnHeaderLabel( - column: Column -): string { - const meta = column.columnDef.meta as { headerTitle?: string } | undefined - if (typeof meta?.headerTitle === "string") return meta.headerTitle - const defHeader = column.columnDef.header - if (typeof defHeader === "string") return defHeader - return String(column.id) -} - -export type DataGridApiFetchParams = { - pageIndex: number - pageSize: number - sorting?: SortingState - filters?: ColumnFiltersState - searchQuery?: string -} - -export type DataGridApiResponse = { - data: T[] - empty: boolean - pagination: { - total: number - page: number - } -} - -export interface DataGridContextProps { - props: DataGridProps - table: Table - recordCount: number - isLoading: boolean -} - -export type DataGridRequestParams = { - pageIndex: number - pageSize: number - sorting?: SortingState - columnFilters?: ColumnFiltersState -} - -export interface DataGridProps { - className?: string - table?: Table - recordCount: number - children?: ReactNode - onRowClick?: (row: TData) => void - isLoading?: boolean - loadingMode?: "skeleton" | "spinner" - loadingMessage?: ReactNode | string - fetchingMoreMessage?: ReactNode | string - allRowsLoadedMessage?: ReactNode | string - emptyMessage?: ReactNode | string - tableLayout?: { - dense?: boolean - cellBorder?: boolean - rowBorder?: boolean - rowRounded?: boolean - stripped?: boolean - headerBackground?: boolean - headerBorder?: boolean - headerSticky?: boolean - width?: "auto" | "fixed" - columnsVisibility?: boolean - columnsResizable?: boolean - columnsResizeMode?: "onChange" | "onEnd" - columnsPinnable?: boolean - columnsMovable?: boolean - columnsDraggable?: boolean - rowsDraggable?: boolean - rowsPinnable?: boolean - } - tableClassNames?: { - base?: string - header?: string - headerRow?: string - headerSticky?: string - body?: string - bodyRow?: string - footer?: string - edgeCell?: string - } -} - -const DataGridContext = createContext< - // eslint-disable-next-line @typescript-eslint/no-explicit-any - DataGridContextProps | undefined ->(undefined) - -function useDataGrid() { - const context = useContext(DataGridContext) - if (!context) { - throw new Error("useDataGrid must be used within a DataGridProvider") - } - return context -} - -function DataGridProvider({ - children, - table, - ...props -}: DataGridProps & { table: Table }) { - const tableState = table.getState() - const resolvedColumnsResizeMode = - props.tableLayout?.columnsResizeMode ?? "onEnd" - - // Keep resize mode aligned with the DataGrid contract every render so - // consumer-level useReactTable options cannot flip it back between drags. - if (props.tableLayout?.columnsResizable) { - table.options.columnResizeMode = resolvedColumnsResizeMode - } - - // Memoize context value so consumers don't re-render during column resize. - // Column sizing state is intentionally excluded from deps -- CSS variables - // on the
element handle width updates without React re-renders. - const value = useMemo( - () => ({ - props, - table, - recordCount: props.recordCount, - isLoading: props.isLoading || false, - }), - // eslint-disable-next-line react-hooks/exhaustive-deps - [ - table, - props.recordCount, - props.isLoading, - props.loadingMode, - props.loadingMessage, - props.fetchingMoreMessage, - props.allRowsLoadedMessage, - props.emptyMessage, - props.onRowClick, - props.className, - // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify(props.tableLayout), - // eslint-disable-next-line react-hooks/exhaustive-deps - JSON.stringify(props.tableClassNames), - tableState.sorting, - tableState.pagination, - tableState.columnFilters, - tableState.rowSelection, - tableState.expanded, - tableState.columnVisibility, - tableState.columnOrder, - tableState.columnPinning, - tableState.globalFilter, - ] - ) - - return ( - - {children} - - ) -} - -function DataGrid({ - children, - table, - ...props -}: DataGridProps) { - const defaultProps: Partial> = { - loadingMode: "skeleton", - tableLayout: { - dense: false, - cellBorder: false, - rowBorder: true, - rowRounded: false, - stripped: false, - headerSticky: false, - headerBackground: true, - headerBorder: true, - width: "fixed", - columnsVisibility: false, - columnsResizable: false, - columnsResizeMode: "onEnd", - columnsPinnable: false, - columnsMovable: false, - columnsDraggable: false, - rowsDraggable: false, - rowsPinnable: false, - }, - tableClassNames: { - base: "", - header: "", - headerRow: "", - headerSticky: "sticky top-0 z-15 bg-background/90 backdrop-blur-xs", - body: "", - bodyRow: "", - footer: "", - edgeCell: "", - }, - } - - const mergedProps: DataGridProps = { - ...defaultProps, - ...props, - tableLayout: { - ...defaultProps.tableLayout, - ...(props.tableLayout || {}), - }, - tableClassNames: { - ...defaultProps.tableClassNames, - ...(props.tableClassNames || {}), - }, - } - - // Ensure table is provided - if (!table) { - throw new Error('DataGrid requires a "table" prop') - } - - return ( - - {children} - - ) -} - -function DataGridContainer({ - children, - className, - border = true, -}: { - children: ReactNode - className?: string - border?: boolean -}) { - return ( -
- {children} -
- ) -} - -export { useDataGrid, DataGridProvider, DataGrid, DataGridContainer } \ No newline at end of file diff --git a/components/reui/legacy-data-grid/index.ts b/components/reui/legacy-data-grid/index.ts deleted file mode 100644 index 9f45e474..00000000 --- a/components/reui/legacy-data-grid/index.ts +++ /dev/null @@ -1,36 +0,0 @@ -export { - DataGrid, - DataGridContainer, - DataGridProvider, - useDataGrid, - getColumnHeaderLabel, - type DataGridProps, - type DataGridApiFetchParams, - type DataGridApiResponse, -} from "./data-grid" -export { DataGridColumnFilter } from "./data-grid-column-filter" -export { DataGridColumnHeader } from "./data-grid-column-header" -export { DataGridColumnVisibility } from "./data-grid-column-visibility" -export { DataGridPagination } from "./data-grid-pagination" -export { DataGridScrollArea } from "./data-grid-scroll-area" -export { DataGridTableDndRowHandle, DataGridTableDndRows } from "./data-grid-table-dnd-rows" -export { DataGridTableDnd } from "./data-grid-table-dnd" -export { DataGridTableVirtual } from "./data-grid-table-virtual" -export { - DataGridTable, - DataGridTableBase, - DataGridTableBody, - DataGridTableBodyRow, - DataGridTableBodyRowCell, - DataGridTableBodyRowExpandded, - DataGridTableEmpty, - DataGridTableFoot, - DataGridTableFootRow, - DataGridTableFootRowCell, - DataGridTableHead, - DataGridTableHeadRow, - DataGridTableHeadRowCell, - DataGridTableRowSelect, - DataGridTableRowSelectAll, - getDataGridTableRowSections, -} from "./data-grid-table" diff --git a/docs/ux-ui-progress.md b/docs/ux-ui-progress.md index a1f409fb..39f788f0 100644 --- a/docs/ux-ui-progress.md +++ b/docs/ux-ui-progress.md @@ -312,6 +312,14 @@ Production build блока 7: `npm run build` прошёл, 34 статичес Квота: новое окно 0% на старте, 13% использовано после проверок, недельная — 91%. Остаток Data Grid: `components/data-table.tsx`, layout-импорты `DataPageCard`/`DataPageToolbar` и затем удаление общей legacy-основы/v8. Старые Filters остаются у BGP, statistics, backups, backups-history и в fallback DataPageToolbar. Этап 4 не завершён; сохранить блок локальным коммитом без push. +## Этап 4 — блок 11: удаление старого Data Grid (2026-09-14) + +Мигрирована общая `DataTable` с сохранением опциональной клиентской пагинации (`manualPagination: !pagination`). `DataPageCard` и `DataPageToolbar` используют современный layout; значения CSS-классов совпадают с прежними. Удалены 14 файлов неиспользуемой legacy-копии и обёрток, а также alias `@tanstack/react-table-v8` и его вложенная зависимость из package.json/lockfile. Версия v9 остаётся 9.2.4. Поиск по app/components/lib/package.json/package-lock.json больше не находит импортов legacy Data Grid/v8. + +Проверки после удаления общей основы: TypeScript и ESLint изменённых обёрток — успешно; вся браузерная регрессия — **49 passed**; production build — успешно, 34 статические страницы. У `DataTable` сейчас нет потребителей, её generic API проверен компилятором; пагинация рабочих таблиц покрыта регрессией справочников. Next.js сообщил о восстановлении SWC lockfile, итоговый diff lockfile содержит только удаление v8 и вложенного table-core. Старый Filters ещё не удалён: этап 4 остаётся открытым, ветку не выпускать. + +Следующий блок — BGP и backups/backups-history на FilterQuery, затем statistics с явным ограничением на поддерживаемые API-срезы (AND равенств) и удаление старого Filters/fallback. Сохранить текущий блок отдельным локальным коммитом без push. + ## Карта миграции (исходная инвентаризация этапа 0) Текущая связка: @tanstack/react-table ^8.21.3, @tanstack/react-virtual ^3.14.4, base-nova. Актуальный ReUI MCP ранее подтвердил useTable/dataGridFeatures для Data Grid и дерево FilterQuery вместо filters/onChange для Filters. Перед установкой перепроверить совместимые версии, сохранить семантику существующих фильтров (простые условия → AND) и закрепить lockfile. diff --git a/package-lock.json b/package-lock.json index d29f2d8a..591cf0fc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,7 +20,6 @@ "@dnd-kit/utilities": "^3.2.2", "@mmapp/contracts": "1.0.0", "@tanstack/react-table": "9.2.4", - "@tanstack/react-table-v8": "npm:@tanstack/react-table@8.21.3", "@tanstack/react-virtual": "^3.14.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", @@ -4552,40 +4551,6 @@ "react": ">=18" } }, - "node_modules/@tanstack/react-table-v8": { - "name": "@tanstack/react-table", - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/react-table/-/react-table-8.21.3.tgz", - "integrity": "sha512-5nNMTSETP4ykGegmVkhjcS8tTLW6Vl4axfEGQN3v0zdHYbK4UfoqfPChclTrJ4EoK9QynqAu9oUf8VEmrpZ5Ww==", - "license": "MIT", - "dependencies": { - "@tanstack/table-core": "8.21.3" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - }, - "peerDependencies": { - "react": ">=16.8", - "react-dom": ">=16.8" - } - }, - "node_modules/@tanstack/react-table-v8/node_modules/@tanstack/table-core": { - "version": "8.21.3", - "resolved": "https://registry.npmjs.org/@tanstack/table-core/-/table-core-8.21.3.tgz", - "integrity": "sha512-ldZXEhOBb8Is7xLs01fR3YEc3DERiz5silj8tnGkFZytt1abEvl/GhUmCE0PMLaMPTa3Jk4HbKmRlHmu+gCftg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/tannerlinsley" - } - }, "node_modules/@tanstack/react-table/node_modules/@tanstack/react-store": { "version": "0.11.1", "resolved": "https://registry.npmjs.org/@tanstack/react-store/-/react-store-0.11.1.tgz", diff --git a/package.json b/package.json index c7c60a02..4739bef0 100644 --- a/package.json +++ b/package.json @@ -23,7 +23,6 @@ "@dnd-kit/utilities": "^3.2.2", "@mmapp/contracts": "1.0.0", "@tanstack/react-table": "9.2.4", - "@tanstack/react-table-v8": "npm:@tanstack/react-table@8.21.3", "@tanstack/react-virtual": "^3.14.4", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1",