feat(web): upgrade lists to ReUI Table v9 and EventCalendar
quality / commitlint (push) Skipped
quality / changes (push) Successful in 9s
quality / docker-check (push) Skipped
quality / openapi (push) Successful in 39s
quality / web (push) Successful in 1m29s
quality / go (push) Successful in 1m3s
quality / bird2 (push) Successful in 15s
CD / quality (push) Successful in 3m43s
CD / publish (push) Failing after 8m29s

Единый kitDataGridTableLayout и ResourcePage/FrameDataGrid на всех списках.
Календарь задач переведён на EventCalendar, lookup — на cascader, у операций появился вид доски.
Удалены settings-7 и самописные DataGridSection/toolbar.

Co-authored-by: Cursor <[email protected]>
This commit is contained in:
Denozordec
2026-08-23 01:38:56 +07:00
co-authored by Cursor
parent 927e27640a
commit d56405af7b
157 changed files with 39067 additions and 6110 deletions
@@ -1,52 +1,461 @@
import type { ReactNode } from 'react'
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 { cn } from '@evobgp/ui/lib/utils'
import { EmptyState } from '@/components/empty-state'
import { DATA_GRID_PAGINATION_RU } from '@/lib/data-grid-defaults'
export type DataGridColumnDef<TData extends object> = ColumnDef<DataGridFeatures, TData>
/**
* Frame shell for list/grid sections (replaces DataGridCard Card-named API).
* @see https://reui.io/preview/base/data-grid-filtering-2
* @see https://reui.io/docs/components/base/frame
* Единый 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 FrameDataGrid({
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 — единственный tableClassNames в kit, не в domain. */
export const kitDataGridTableClassNames = {
edgeCell: 'first:ps-3 last:pe-3',
} as const
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<T extends object>(
columns: DataGridColumnDef<T>[],
columnPinControls: boolean,
): DataGridColumnDef<T>[] {
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<T>
})
}
export interface FrameDataGridProps<TData extends object> {
title?: ReactNode
description?: ReactNode
actions?: ReactNode
columns: DataGridColumnDef<TData>[]
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<ColumnVisibilityState>
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,
children,
className,
}: {
title?: ReactNode
description?: ReactNode
actions?: ReactNode
children: ReactNode
className?: string
}) {
const hasHeader = Boolean(title || description || actions)
if (!title && !description && !actions) return null
return (
<Frame dense spacing="sm" className={cn('w-full min-w-0 gap-0 p-0', className)}>
<FramePanel className="flex flex-col gap-0 p-0 shadow-xs">
{hasHeader ? (
<FrameHeader className="flex-col items-stretch gap-3 border-b sm:flex-row sm:items-start sm:justify-between">
<div className="flex min-w-0 flex-1 flex-col gap-px">
{title ? <FrameTitle>{title}</FrameTitle> : null}
{description ? (
<FrameDescription>{description}</FrameDescription>
) : null}
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">
{actions}
</div>
) : null}
</FrameHeader>
<FrameHeader className="flex-row items-start justify-between gap-3">
<div className="flex min-w-0 flex-col gap-px">
{title ? <FrameTitle className="text-balance">{title}</FrameTitle> : null}
{description ? (
<FrameDescription className="text-xs text-pretty">{description}</FrameDescription>
) : null}
<div className="p-0">{children}</div>
</div>
{actions ? (
<div className="flex shrink-0 flex-wrap items-center justify-end gap-2">{actions}</div>
) : null}
</FrameHeader>
)
}
function FrameDataGridBody<TData extends object>({
table,
data,
emptyTitle,
onRowClick,
dense,
virtualization,
height,
footerContent,
showPagination,
enableColumnVisibility,
columnsPinnable,
tableWidth,
}: {
table: DataGridTableInstance<TData>
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 ? (
<DataGridTableVirtual height={height} footerContent={footerContent} />
) : (
<DataGridTable footerContent={footerContent} />
)
return (
<DataGrid
table={table}
recordCount={data.length}
onRowClick={onRowClick}
emptyMessage={emptyTitle}
tableLayout={kitDataGridTableLayout({
dense,
width: tableWidth,
columnsPinnable,
columnsVisibility: enableColumnVisibility,
})}
tableClassNames={kitDataGridTableClassNames}
>
{virtualization ? (
<DataGridScrollArea orientation="vertical" style={{ height }}>
{tableNode}
</DataGridScrollArea>
) : (
<DataGridScrollArea>{tableNode}</DataGridScrollArea>
)}
{showPagination ? (
<>
<Separator />
<FrameFooter>
<DataGridPagination {...DATA_GRID_PAGINATION_RU} />
</FrameFooter>
</>
) : null}
</DataGrid>
)
}
export function FrameDataGrid<TData extends object>({
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,
tableWidth = 'fixed',
columnPinControls = false,
}: FrameDataGridProps<TData>) {
const showPagination = pagination ?? true
const [sorting, setSorting] = useState<SortingState>(initialSorting ?? [])
const [rowSelection, setRowSelection] = useState<RowSelectionState>({})
const [expanded, setExpanded] = useState<ExpandedState>({})
const [paginationState, setPaginationState] = useState<PaginationState>({
pageIndex: 0,
pageSize: showPagination ? pageSize : Number.POSITIVE_INFINITY,
})
const [internalColumnVisibility, setInternalColumnVisibility] = useState<ColumnVisibilityState>(
() => {
const stored = columnVisibilityStorageKey
? loadStoredColumnVisibility(columnVisibilityStorageKey)
: undefined
return { ...initialColumnVisibility, ...stored }
},
)
const isColumnVisibilityControlled = columnVisibilityProp !== undefined
const columnVisibility = isColumnVisibilityControlled
? columnVisibilityProp
: internalColumnVisibility
const setColumnVisibility: OnChangeFn<ColumnVisibilityState> = 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<TData> = {
id: 'select',
header: () => <DataGridTableRowSelectAll />,
cell: ({ row }) => <DataGridTableRowSelect row={row} />,
enableSorting: false,
enableHiding: false,
size: 40,
meta: { cellClassName: 'w-10' },
}
const expandColumn: DataGridColumnDef<TData> = {
id: 'expand',
header: () => null,
cell: ({ row }) => <DataGridTableRowExpand row={row} />,
enableSorting: false,
enableHiding: false,
size: 40,
meta: {
cellClassName: 'w-10',
expandedContent,
},
}
const tableColumns: DataGridColumnDef<TData>[] = applyColumnPinControls(
[
...(expandedContent ? [expandColumn] : []),
...(enableRowSelection ? [selectColumn] : []),
...columns,
],
columnPinControls,
)
const lastColId = pinLastColumn ? (tableColumns[tableColumns.length - 1]?.id ?? '') : ''
const pinLeft = pinLeftColumnIds ?? []
const enablePinning = pinLastColumn || pinLeft.length > 0
const columnPinning = {
start: pinLeft,
end: pinLastColumn && lastColId ? [lastColId] : [],
}
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 ? (
<DataGridColumnVisibility
table={table}
trigger={
<Button variant="ghost" size="sm">
<Columns3Icon data-icon="inline-start" />
Колонки
</Button>
}
/>
) : null
const headerActions = actions ? (
<div className="flex items-center gap-2">
{columnVisibilityAction}
{actions}
</div>
) : (
columnVisibilityAction
)
const hasHeader = Boolean(title || description || actions || showColumnVisibilityTrigger)
if (data.length === 0) {
return (
<Frame dense variant="default" spacing="sm" className={cn('w-full', className)}>
{hasHeader ? (
<DataGridSectionHeader title={title} description={description} actions={headerActions} />
) : null}
<FramePanel className="flex min-h-72 w-full flex-col items-center justify-center">
<EmptyState title={emptyTitle} description={emptyDescription} action={emptyAction} />
</FramePanel>
</Frame>
)
}
return (
<Frame dense variant="default" spacing="sm" className={cn('w-full', className)}>
{hasHeader ? (
<DataGridSectionHeader title={title} description={description} actions={headerActions} />
) : null}
<FramePanel className="p-0 shadow-none!">
<FrameDataGridBody
table={table}
data={data}
emptyTitle={emptyTitle}
onRowClick={onRowClick}
dense={dense}
virtualization={virtualization}
height={height}
footerContent={footerContent}
showPagination={showPagination}
enableColumnVisibility={enableColumnVisibility}
columnsPinnable={enablePinning}
tableWidth={tableWidth}
/>
</FramePanel>
</Frame>
)