feat(web): ReUI DataGrid — Column Icons, оболочка и хелперы ячеек

DataGridColumnHeader в columnDefFromDataTable, bordered-контейнер, русская пагинация, хелперы dataGridCellStack/WithIcon/WithFlag.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-06-27 01:42:39 +07:00
co-authored by Cursor
parent bbb6de29be
commit a6f202f659
3 changed files with 119 additions and 31 deletions
+72 -31
View File
@@ -1,4 +1,4 @@
import { useMemo, useState, type ReactNode } from 'react'
import { useState, type ReactNode } from 'react'
import {
useReactTable,
getCoreRowModel,
@@ -18,7 +18,22 @@ import { DataGridTable } from '@/components/reui/data-grid/data-grid-table'
import { DataGridTableVirtual } from '@/components/reui/data-grid/data-grid-table-virtual'
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
import { DataGridColumnHeader } from '@/components/reui/data-grid/data-grid-column-header'
import { EmptyState } from './empty-state'
import type { DataTableColumn } from './data-table-card'
const PAGINATION_LABELS = {
rowsPerPageLabel: 'Строк на странице',
info: '{from}{to} из {count}',
previousPageLabel: 'Предыдущая страница',
nextPageLabel: 'Следующая страница',
} as const
function resolveHeaderTitle(header: ReactNode, headerTitle?: string): string {
if (headerTitle) return headerTitle
if (typeof header === 'string') return header
return ''
}
export interface DataGridCardProps<TData extends object> {
title?: ReactNode
@@ -32,9 +47,9 @@ export interface DataGridCardProps<TData extends object> {
emptyDescription?: string
emptyAction?: ReactNode
onRowClick?: (row: TData) => void
/** Включить пагинацию. По умолчанию true для >25 строк. */
/** Включить пагинацию. По умолчанию true. */
pagination?: boolean
/** Размер страницы. По умолчанию 25. */
/** Размер страницы. По умолчанию 10. */
pageSize?: number
/** Footer-контент (итоги). */
footerContent?: ReactNode
@@ -51,6 +66,14 @@ export interface DataGridCardProps<TData extends object> {
className?: string
}
function DataGridPaginationBar() {
return (
<div className="border-t px-4 py-2.5">
<DataGridPagination {...PAGINATION_LABELS} />
</div>
)
}
export function DataGridCard<TData extends object>({
title,
description,
@@ -63,7 +86,7 @@ export function DataGridCard<TData extends object>({
emptyAction,
onRowClick,
pagination,
pageSize = 25,
pageSize = 10,
footerContent,
dense = false,
pinLastColumn = false,
@@ -76,16 +99,11 @@ export function DataGridCard<TData extends object>({
const lastColId = pinLastColumn ? columns[columns.length - 1]?.id ?? '' : ''
const columnsWithIds = useMemo(() => {
if (rowId) return columns
return columns
}, [columns, rowId])
const showPagination = pagination ?? data.length > pageSize
const showPagination = pagination ?? true
const table = useReactTable<TData>({
data,
columns: columnsWithIds,
columns,
state: { sorting },
onSortingChange: setSorting,
getCoreRowModel: getCoreRowModel(),
@@ -131,8 +149,8 @@ export function DataGridCard<TData extends object>({
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
</CardHeader>
)}
<CardContent className="p-0">
<DataGridContainer className="border-0 rounded-none">
<CardContent className={dense ? 'p-3' : 'p-4'}>
<DataGridContainer>
<DataGrid
table={table}
recordCount={data.length}
@@ -154,13 +172,16 @@ export function DataGridCard<TData extends object>({
}}
>
{virtualization ? (
<DataGridScrollArea orientation="vertical" style={{ height }}>
<DataGridTableVirtual height={height} footerContent={footerContent} />
</DataGridScrollArea>
<>
<DataGridScrollArea orientation="vertical" style={{ height }}>
<DataGridTableVirtual height={height} footerContent={footerContent} />
</DataGridScrollArea>
{showPagination ? <DataGridPaginationBar /> : null}
</>
) : (
<>
<DataGridTable footerContent={footerContent} />
{showPagination ? <DataGridPagination /> : null}
{showPagination ? <DataGridPaginationBar /> : null}
</>
)}
</DataGrid>
@@ -170,22 +191,42 @@ export function DataGridCard<TData extends object>({
)
}
/** Хелпер для конвертации старых DataTableColumn<T> → ColumnDef<T>. */
/** Хелпер для конвертации DataTableColumn<T> → ColumnDef<T> с DataGridColumnHeader. */
export function columnDefFromDataTable<T>(
cols: {
key: string
header: ReactNode
cell: (row: T, index: number) => ReactNode
className?: string
headerClassName?: string
}[],
cols: DataTableColumn<T>[],
): ColumnDef<T, unknown>[] {
return cols.map((c) => ({
id: c.key,
header: () => c.header,
cell: ({ row }) => c.cell(row.original, row.index),
meta: { className: c.className, headerClassName: c.headerClassName },
}))
return cols.map((c) => {
const title = resolveHeaderTitle(c.header, c.headerTitle)
const Icon = c.icon
const sortable = c.sortable ?? Boolean(Icon)
return {
id: c.key,
...(sortable
? {
accessorFn: c.sortValue
? (row: T) => c.sortValue!(row)
: (row: T) => (row as Record<string, unknown>)[c.key] as string | number,
}
: {}),
header: Icon
? ({ column }) => (
<DataGridColumnHeader
column={column}
title={title}
icon={<Icon />}
/>
)
: () => c.header,
cell: ({ row }) => c.cell(row.original, row.index),
enableSorting: sortable,
meta: {
headerTitle: title || undefined,
cellClassName: c.className,
headerClassName: c.headerClassName,
},
}
})
}
/** re-export flexRender для удобства использования в колонках. */
@@ -0,0 +1,42 @@
import type { ReactNode } from 'react'
import { cn } from '@cfdm/ui/lib/utils'
export function dataGridCellStack(
primary: ReactNode,
secondary?: ReactNode,
className?: string,
) {
return (
<div className={cn('flex flex-col', className)}>
<span className="font-medium">{primary}</span>
{secondary ? <span className="text-xs text-muted-foreground">{secondary}</span> : null}
</div>
)
}
export function dataGridCellWithIcon(
icon: ReactNode,
children: ReactNode,
className?: string,
) {
return (
<div className={cn('flex items-center gap-2', className)}>
<span className="shrink-0 text-muted-foreground">{icon}</span>
{children}
</div>
)
}
export function dataGridCellWithFlag(
flag: ReactNode,
primary: ReactNode,
secondary?: ReactNode,
) {
return (
<div className="flex items-center gap-2">
<span className="size-4 shrink-0 leading-none">{flag}</span>
{secondary ? dataGridCellStack(primary, secondary) : <span className="font-medium">{primary}</span>}
</div>
)
}
@@ -1,4 +1,5 @@
import type { ReactNode } from 'react'
import type { LucideIcon } from 'lucide-react'
import {
Table,
TableBody,
@@ -14,6 +15,10 @@ export interface DataTableColumn<T> {
key: string
header: ReactNode
cell: (row: T, index: number) => ReactNode
icon?: LucideIcon
sortable?: boolean
sortValue?: (row: T) => string | number
headerTitle?: string
className?: string
headerClassName?: string
}