import { cloneElement, isValidElement, useEffect, useState, type ReactElement, type ReactNode, } from 'react' import { useTable, type ColumnDef, type ColumnVisibilityState, type ExpandedState, type OnChangeFn, type PaginationState, type RowSelectionState, type SortingState, } from '@tanstack/react-table' import { Columns3Icon } from 'lucide-react' import { Button } from '@evobgp/ui/components/button' import { Separator } from '@evobgp/ui/components/separator' import { cn } from '@evobgp/ui/lib/utils' import { DataGrid, dataGridFeatures, type DataGridFeatures, type DataGridTableInstance, } from '@/components/reui/data-grid/data-grid' import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header' import { DataGridColumnVisibility } from '@/components/reui/data-grid/data-grid-column-visibility' import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination' import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area' import { DataGridTable, DataGridTableRowExpand, DataGridTableRowSelect, DataGridTableRowSelectAll, } from '@/components/reui/data-grid/data-grid-table' import { DataGridTableVirtual } from '@/components/reui/data-grid/data-grid-table-virtual' import { Frame, FrameDescription, FrameFooter, FrameHeader, FramePanel, FrameTitle, } from '@/components/reui/frame' import { EmptyState } from '@/components/empty-state' import { DATA_GRID_PAGINATION_RU } from '@/lib/data-grid-defaults' export type DataGridColumnDef = ColumnDef /** * Единый tableLayout для всех ops-гридов. * Visual SoT: установленный data-grid-filtering-2 (`dense: true`, без zebra). * Preview: https://reui.io/preview/base/data-grid-filtering-2 * Docs: https://reui.io/docs/components/base/data-grid */ export function kitDataGridTableLayout( opts: { dense?: boolean width?: 'fixed' | 'auto' columnsPinnable?: boolean columnsVisibility?: boolean } = {}, ) { return { dense: opts.dense ?? true, rowBorder: true, headerSticky: false, headerBackground: false, headerBorder: true, width: opts.width ?? ('fixed' as const), columnsVisibility: opts.columnsVisibility ?? false, columnsResizable: false, columnsPinnable: opts.columnsPinnable ?? false, columnsMovable: false, rowsDraggable: false, rowsPinnable: false, } } /** filtering-2 edge inset + Frame-surface pinned cells (not page --background). */ export const kitDataGridTableClassNames = { base: '[&_[data-pinned]]:bg-(--frame-panel-bg)', edgeCell: 'first:ps-3 last:pe-3', } as const /** * Compact action column size from data-grid-filtering-2. * Preview: https://reui.io/preview/base/data-grid-filtering-2 */ export const KIT_ACTION_COLUMN_SIZE = 56 const ACTION_CELL_ALIGN = 'flex items-center justify-end' function lastColumnId(columns: DataGridColumnDef[]): string { const last = columns[columns.length - 1] if (!last) return '' if (last.id) return last.id if ('accessorKey' in last && typeof last.accessorKey === 'string') return last.accessorKey return '' } function wrapActionCell( cell: DataGridColumnDef['cell'], ): DataGridColumnDef['cell'] { if (typeof cell !== 'function') { return () =>
{cell as ReactNode}
} return (ctx) =>
{cell(ctx)}
} /** * filtering-2 action column DNA: locked width, no sort/resize, inner justify-end * (flex on the cell wrapper, never on `td` — that breaks rowBorder alignment). * Preview: https://reui.io/preview/base/data-grid-filtering-2 */ export function applyKitActionColumn( columns: DataGridColumnDef[], opts: { pinLastColumn?: boolean } = {}, ): DataGridColumnDef[] { if (columns.length === 0) return columns const last = columns[columns.length - 1] const lastId = lastColumnId(columns) if (lastId !== 'actions' && !opts.pinLastColumn) return columns const size = last.size ?? KIT_ACTION_COLUMN_SIZE return [ ...columns.slice(0, -1), { ...last, size, minSize: last.minSize ?? size, maxSize: last.maxSize ?? size, enableSorting: false, enableResizing: false, cell: last.cell ? wrapActionCell(last.cell) : last.cell, }, ] } /** End-pin only when the grid actually scrolls horizontally (not for action columns). */ export function kitColumnPinning(opts: { pinLastColumn?: boolean horizontalScroll?: boolean lastColId: string pinLeftColumnIds?: string[] }): { enablePinning: boolean columnPinning: { start: string[]; end: string[] } } { const pinLeft = opts.pinLeftColumnIds ?? [] const pinEnd = Boolean(opts.pinLastColumn && opts.horizontalScroll && opts.lastColId) return { enablePinning: pinEnd || pinLeft.length > 0, columnPinning: { start: pinLeft, end: pinEnd ? [opts.lastColId] : [], }, } } function loadStoredColumnVisibility(key: string): ColumnVisibilityState | undefined { try { const raw = localStorage.getItem(key) if (!raw) return undefined return JSON.parse(raw) as ColumnVisibilityState } catch { return undefined } } export { loadStoredColumnVisibility } function applyColumnPinControls( columns: DataGridColumnDef[], columnPinControls: boolean, ): DataGridColumnDef[] { return columns.map((col) => { const origHeader = col.header if (typeof origHeader !== 'function') return col return { ...col, header: (ctx) => { const node = origHeader(ctx) if (isValidElement(node) && node.type === DataGridColumnHeader) { return cloneElement(node as ReactElement<{ pinnable?: boolean }>, { pinnable: columnPinControls, }) } return node }, } as DataGridColumnDef }) } export interface FrameDataGridProps { title?: ReactNode description?: ReactNode actions?: ReactNode columns: DataGridColumnDef[] data: TData[] rowId?: (row: TData, index: number) => string emptyTitle?: string emptyDescription?: string emptyAction?: ReactNode onRowClick?: (row: TData) => void pagination?: boolean pageSize?: number footerContent?: ReactNode dense?: boolean pinLastColumn?: boolean initialSorting?: SortingState virtualization?: boolean height?: number enableRowSelection?: boolean onRowSelectionChange?: (selectedIds: string[]) => void enableColumnVisibility?: boolean columnVisibility?: ColumnVisibilityState onColumnVisibilityChange?: OnChangeFn columnVisibilityTrigger?: boolean columnVisibilityStorageKey?: string initialColumnVisibility?: ColumnVisibilityState className?: string expandedContent?: (row: TData) => ReactNode getRowCanExpand?: (row: TData) => boolean pinLeftColumnIds?: string[] horizontalScroll?: boolean tableWidth?: 'fixed' | 'auto' columnPinControls?: boolean isLoading?: boolean } function DataGridSectionHeader({ title, description, actions, }: { title?: ReactNode description?: ReactNode actions?: ReactNode }) { if (!title && !description && !actions) return null return (
{title ? {title} : null} {description ? ( {description} ) : null}
{actions ? (
{actions}
) : null}
) } function FrameDataGridBody({ table, data, emptyTitle, onRowClick, dense, virtualization, height, footerContent, showPagination, enableColumnVisibility, columnsPinnable, tableWidth, }: { table: DataGridTableInstance data: TData[] emptyTitle: string onRowClick?: (row: TData) => void dense: boolean virtualization: boolean height: number footerContent?: ReactNode showPagination: boolean enableColumnVisibility: boolean columnsPinnable: boolean tableWidth: 'fixed' | 'auto' }) { const tableNode = virtualization ? ( ) : ( ) return ( {virtualization ? ( {tableNode} ) : ( {tableNode} )} {showPagination ? ( <> ) : null} ) } export function FrameDataGrid({ title, description, actions, columns, data, rowId, emptyTitle = 'Нет записей', emptyDescription, emptyAction, onRowClick, pagination, pageSize = 10, footerContent, dense = true, pinLastColumn = false, initialSorting, virtualization = false, height = 480, enableRowSelection = false, onRowSelectionChange, enableColumnVisibility = false, columnVisibility: columnVisibilityProp, onColumnVisibilityChange, columnVisibilityTrigger, columnVisibilityStorageKey, initialColumnVisibility, className, expandedContent, getRowCanExpand, pinLeftColumnIds, horizontalScroll = false, tableWidth = 'fixed', columnPinControls = false, }: FrameDataGridProps) { const showPagination = pagination ?? true const [sorting, setSorting] = useState(initialSorting ?? []) const [rowSelection, setRowSelection] = useState({}) const [expanded, setExpanded] = useState({}) const [paginationState, setPaginationState] = useState({ pageIndex: 0, pageSize: showPagination ? pageSize : Number.POSITIVE_INFINITY, }) const [internalColumnVisibility, setInternalColumnVisibility] = useState( () => { const stored = columnVisibilityStorageKey ? loadStoredColumnVisibility(columnVisibilityStorageKey) : undefined return { ...initialColumnVisibility, ...stored } }, ) const isColumnVisibilityControlled = columnVisibilityProp !== undefined const columnVisibility = isColumnVisibilityControlled ? columnVisibilityProp : internalColumnVisibility const setColumnVisibility: OnChangeFn = isColumnVisibilityControlled ? (onColumnVisibilityChange ?? (() => undefined)) : setInternalColumnVisibility useEffect(() => { setPaginationState((current) => ({ pageIndex: showPagination ? current.pageIndex : 0, pageSize: showPagination ? pageSize : Number.POSITIVE_INFINITY, })) }, [pageSize, showPagination]) useEffect(() => { if (isColumnVisibilityControlled || !columnVisibilityStorageKey) return localStorage.setItem(columnVisibilityStorageKey, JSON.stringify(columnVisibility)) }, [columnVisibility, columnVisibilityStorageKey, isColumnVisibilityControlled]) const selectColumn: DataGridColumnDef = { id: 'select', header: () => , cell: ({ row }) => , enableSorting: false, enableHiding: false, size: 40, meta: { cellClassName: 'w-10' }, } const expandColumn: DataGridColumnDef = { id: 'expand', header: () => null, cell: ({ row }) => , enableSorting: false, enableHiding: false, size: 40, meta: { cellClassName: 'w-10', expandedContent, }, } const tableColumns: DataGridColumnDef[] = applyColumnPinControls( applyKitActionColumn( [ ...(expandedContent ? [expandColumn] : []), ...(enableRowSelection ? [selectColumn] : []), ...columns, ], { pinLastColumn }, ), columnPinControls, ) const { enablePinning, columnPinning } = kitColumnPinning({ pinLastColumn, horizontalScroll, lastColId: lastColumnId(tableColumns), pinLeftColumnIds, }) const table = useTable({ features: dataGridFeatures, data, columns: tableColumns, state: { sorting, pagination: paginationState, columnVisibility, expanded, ...(enablePinning ? { columnPinning } : {}), ...(enableRowSelection ? { rowSelection } : {}), }, onSortingChange: setSorting, onPaginationChange: setPaginationState, onExpandedChange: setExpanded, onColumnVisibilityChange: setColumnVisibility, onRowSelectionChange: enableRowSelection ? (updater) => { setRowSelection((prev) => { const next = typeof updater === 'function' ? updater(prev) : updater if (onRowSelectionChange && rowId) { const ids = Object.keys(next).filter((k) => next[k]) onRowSelectionChange(ids) } return next }) } : undefined, initialState: enablePinning ? { columnPinning } : undefined, getRowId: rowId ? (row, index) => rowId(row, index) : undefined, getRowCanExpand: expandedContent ? (row) => (getRowCanExpand ? getRowCanExpand(row.original) : true) : undefined, enableRowSelection, enableHiding: enableColumnVisibility, }) const showColumnVisibilityTrigger = enableColumnVisibility && (columnVisibilityTrigger ?? true) const columnVisibilityAction = showColumnVisibilityTrigger ? ( Колонки } /> ) : null const headerActions = actions ? (
{columnVisibilityAction} {actions}
) : ( columnVisibilityAction ) const hasHeader = Boolean(title || description || actions || showColumnVisibilityTrigger) if (data.length === 0) { return ( {hasHeader ? ( ) : null} ) } return ( {hasHeader ? ( ) : null} ) }