feat(web): ReUI DataGrid — Column Icons, оболочка и хелперы ячеек
DataGridColumnHeader в columnDefFromDataTable, bordered-контейнер, русская пагинация, хелперы dataGridCellStack/WithIcon/WithFlag. Co-authored-by: Cursor <[email protected]>
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { useMemo, useState, type ReactNode } from 'react'
|
import { useState, type ReactNode } from 'react'
|
||||||
import {
|
import {
|
||||||
useReactTable,
|
useReactTable,
|
||||||
getCoreRowModel,
|
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 { DataGridTableVirtual } from '@/components/reui/data-grid/data-grid-table-virtual'
|
||||||
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
|
import { DataGridScrollArea } from '@/components/reui/data-grid/data-grid-scroll-area'
|
||||||
import { DataGridPagination } from '@/components/reui/data-grid/data-grid-pagination'
|
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 { 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> {
|
export interface DataGridCardProps<TData extends object> {
|
||||||
title?: ReactNode
|
title?: ReactNode
|
||||||
@@ -32,9 +47,9 @@ export interface DataGridCardProps<TData extends object> {
|
|||||||
emptyDescription?: string
|
emptyDescription?: string
|
||||||
emptyAction?: ReactNode
|
emptyAction?: ReactNode
|
||||||
onRowClick?: (row: TData) => void
|
onRowClick?: (row: TData) => void
|
||||||
/** Включить пагинацию. По умолчанию true для >25 строк. */
|
/** Включить пагинацию. По умолчанию true. */
|
||||||
pagination?: boolean
|
pagination?: boolean
|
||||||
/** Размер страницы. По умолчанию 25. */
|
/** Размер страницы. По умолчанию 10. */
|
||||||
pageSize?: number
|
pageSize?: number
|
||||||
/** Footer-контент (итоги). */
|
/** Footer-контент (итоги). */
|
||||||
footerContent?: ReactNode
|
footerContent?: ReactNode
|
||||||
@@ -51,6 +66,14 @@ export interface DataGridCardProps<TData extends object> {
|
|||||||
className?: string
|
className?: string
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function DataGridPaginationBar() {
|
||||||
|
return (
|
||||||
|
<div className="border-t px-4 py-2.5">
|
||||||
|
<DataGridPagination {...PAGINATION_LABELS} />
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
export function DataGridCard<TData extends object>({
|
export function DataGridCard<TData extends object>({
|
||||||
title,
|
title,
|
||||||
description,
|
description,
|
||||||
@@ -63,7 +86,7 @@ export function DataGridCard<TData extends object>({
|
|||||||
emptyAction,
|
emptyAction,
|
||||||
onRowClick,
|
onRowClick,
|
||||||
pagination,
|
pagination,
|
||||||
pageSize = 25,
|
pageSize = 10,
|
||||||
footerContent,
|
footerContent,
|
||||||
dense = false,
|
dense = false,
|
||||||
pinLastColumn = false,
|
pinLastColumn = false,
|
||||||
@@ -76,16 +99,11 @@ export function DataGridCard<TData extends object>({
|
|||||||
|
|
||||||
const lastColId = pinLastColumn ? columns[columns.length - 1]?.id ?? '' : ''
|
const lastColId = pinLastColumn ? columns[columns.length - 1]?.id ?? '' : ''
|
||||||
|
|
||||||
const columnsWithIds = useMemo(() => {
|
const showPagination = pagination ?? true
|
||||||
if (rowId) return columns
|
|
||||||
return columns
|
|
||||||
}, [columns, rowId])
|
|
||||||
|
|
||||||
const showPagination = pagination ?? data.length > pageSize
|
|
||||||
|
|
||||||
const table = useReactTable<TData>({
|
const table = useReactTable<TData>({
|
||||||
data,
|
data,
|
||||||
columns: columnsWithIds,
|
columns,
|
||||||
state: { sorting },
|
state: { sorting },
|
||||||
onSortingChange: setSorting,
|
onSortingChange: setSorting,
|
||||||
getCoreRowModel: getCoreRowModel(),
|
getCoreRowModel: getCoreRowModel(),
|
||||||
@@ -131,8 +149,8 @@ export function DataGridCard<TData extends object>({
|
|||||||
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
|
{actions ? <div className="flex items-center gap-2">{actions}</div> : null}
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
)}
|
)}
|
||||||
<CardContent className="p-0">
|
<CardContent className={dense ? 'p-3' : 'p-4'}>
|
||||||
<DataGridContainer className="border-0 rounded-none">
|
<DataGridContainer>
|
||||||
<DataGrid
|
<DataGrid
|
||||||
table={table}
|
table={table}
|
||||||
recordCount={data.length}
|
recordCount={data.length}
|
||||||
@@ -154,13 +172,16 @@ export function DataGridCard<TData extends object>({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{virtualization ? (
|
{virtualization ? (
|
||||||
|
<>
|
||||||
<DataGridScrollArea orientation="vertical" style={{ height }}>
|
<DataGridScrollArea orientation="vertical" style={{ height }}>
|
||||||
<DataGridTableVirtual height={height} footerContent={footerContent} />
|
<DataGridTableVirtual height={height} footerContent={footerContent} />
|
||||||
</DataGridScrollArea>
|
</DataGridScrollArea>
|
||||||
|
{showPagination ? <DataGridPaginationBar /> : null}
|
||||||
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
<DataGridTable footerContent={footerContent} />
|
<DataGridTable footerContent={footerContent} />
|
||||||
{showPagination ? <DataGridPagination /> : null}
|
{showPagination ? <DataGridPaginationBar /> : null}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</DataGrid>
|
</DataGrid>
|
||||||
@@ -170,22 +191,42 @@ export function DataGridCard<TData extends object>({
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Хелпер для конвертации старых DataTableColumn<T> → ColumnDef<T>. */
|
/** Хелпер для конвертации DataTableColumn<T> → ColumnDef<T> с DataGridColumnHeader. */
|
||||||
export function columnDefFromDataTable<T>(
|
export function columnDefFromDataTable<T>(
|
||||||
cols: {
|
cols: DataTableColumn<T>[],
|
||||||
key: string
|
|
||||||
header: ReactNode
|
|
||||||
cell: (row: T, index: number) => ReactNode
|
|
||||||
className?: string
|
|
||||||
headerClassName?: string
|
|
||||||
}[],
|
|
||||||
): ColumnDef<T, unknown>[] {
|
): ColumnDef<T, unknown>[] {
|
||||||
return cols.map((c) => ({
|
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,
|
id: c.key,
|
||||||
header: () => c.header,
|
...(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),
|
cell: ({ row }) => c.cell(row.original, row.index),
|
||||||
meta: { className: c.className, headerClassName: c.headerClassName },
|
enableSorting: sortable,
|
||||||
}))
|
meta: {
|
||||||
|
headerTitle: title || undefined,
|
||||||
|
cellClassName: c.className,
|
||||||
|
headerClassName: c.headerClassName,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/** re-export flexRender для удобства использования в колонках. */
|
/** 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 { ReactNode } from 'react'
|
||||||
|
import type { LucideIcon } from 'lucide-react'
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -14,6 +15,10 @@ export interface DataTableColumn<T> {
|
|||||||
key: string
|
key: string
|
||||||
header: ReactNode
|
header: ReactNode
|
||||||
cell: (row: T, index: number) => ReactNode
|
cell: (row: T, index: number) => ReactNode
|
||||||
|
icon?: LucideIcon
|
||||||
|
sortable?: boolean
|
||||||
|
sortValue?: (row: T) => string | number
|
||||||
|
headerTitle?: string
|
||||||
className?: string
|
className?: string
|
||||||
headerClassName?: string
|
headerClassName?: string
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user