fix(ui): завершить основу таблиц и фильтров серверов

This commit is contained in:
Denozordec committed 2026-09-13 14:46:17 +07:00
1 parent f9a4a07996
commit 3d3c45e4d2
24 files changed
+385 -263

No files matched your search

@@ -231,8 +231,8 @@ function DataGridScrollArea({
if (!container || !viewport) return
if (!usesCustomVerticalScrollbar) {
resetMetrics()
return
const frame = requestAnimationFrame(resetMetrics)
return () => cancelAnimationFrame(frame)
}
let frame = 0
@@ -480,4 +480,4 @@ function DataGridScrollArea({
}
export { DataGridScrollArea }
export type { DataGridScrollAreaOrientation, DataGridScrollAreaProps }
export type { DataGridScrollAreaOrientation, DataGridScrollAreaProps }
@@ -59,12 +59,11 @@ import {
SortableContext,
sortableKeyboardCoordinates,
useSortable,
verticalListSortingStrategy,
type SortingStrategy,
} from "@dnd-kit/sortable"
import { CSS } from "@dnd-kit/utilities"
import { flexRender } from "@tanstack/react-table"
import type { Cell, HeaderGroup, Row, Table } from "@tanstack/react-table"
import type { Cell, HeaderGroup, Row, } from "@tanstack/react-table"
import { createPortal } from "react-dom"
import { cn } from "@/lib/utils"
@@ -445,14 +444,10 @@ function DataGridTableDndRows<TData extends object>({
// lands offset by that ancestor's own top/left, and the container clamp
// below mis-clamps too, because its rects are measured in viewport space.
//
// Resolved in an effect rather than read at render so the server and the
// first client render agree. A drag cannot start before hydration, so the
// overlay being absent for one frame costs nothing.
// Resolve the portal target on pickup, without a document read during
// hydration or an extra render before the first drag.
const [portalTarget, setPortalTarget] = useState<HTMLElement | null>(null)
useEffect(() => {
setPortalTarget(document.body)
}, [])
// The row being carried, plus the column widths measured off the header the
// moment the drag starts. The clone lives outside the table, so it has no
// columns of its own and has to be told what they are.
@@ -464,6 +459,7 @@ function DataGridTableDndRows<TData extends object>({
} | null>(null)
const pickUpRow = useCallback((id: UniqueIdentifier) => {
setPortalTarget(document.body)
const container = tableContainerRef.current
const head = container?.querySelector("thead tr")
if (!container || !head) {
@@ -737,4 +733,4 @@ function DataGridTableDndRows<TData extends object>({
}
export { DataGridTableDndRowHandle, DataGridTableDndRows }
export type { DataGridTableDndRowData, DataGridTableDndRowDecoration }
export type { DataGridTableDndRowData, DataGridTableDndRowDecoration }
@@ -58,8 +58,7 @@ import type {
Header,
HeaderGroup,
Row,
Table,
} from "@tanstack/react-table"
} from "@tanstack/react-table"
import { Button } from "@/components/ui/button"
import { GripVerticalIcon } from "lucide-react"
@@ -29,7 +29,7 @@ import {
hasDataGridTableRightPinnedColumns,
} from "@/components/reui/data-grid/data-grid-table"
import { flexRender } from "@tanstack/react-table"
import type { Column, Row, Table } from "@tanstack/react-table"
import type { Column, Row, } from "@tanstack/react-table"
import { useVirtualizer } from "@tanstack/react-virtual"
import type {
VirtualItem,
@@ -52,6 +52,8 @@ type DataGridTableVirtualizerInstance = Virtualizer<
type DataGridTableVirtualScrollAlignment = "auto" | "center" | "start" | "end"
const EMPTY_VIRTUAL_ITEMS: never[] = []
interface DataGridTableColumnVirtualizerOptions {
/** Off unless explicitly true; a column window is opt-in. */
enabled?: boolean
@@ -804,17 +806,17 @@ function DataGridTableVirtual<TData extends object>({
// virtualizer's cached sizes by hand.
useEffect(() => {
if (columnVirtualizationActive) columnVirtualizer.measure()
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
columnVirtualizationActive,
columnVirtualizer,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnSizing,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnVisibility,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnOrder,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnPinning,
])
@@ -834,7 +836,7 @@ function DataGridTableVirtual<TData extends object>({
const virtualItems = isVirtualizationEnabled
? virtualizer.getVirtualItems()
: []
: EMPTY_VIRTUAL_ITEMS
const totalSize = isVirtualizationEnabled ? virtualizer.getTotalSize() : 0
const measureRowRef =
isVirtualizationEnabled && customMeasureElement
@@ -1049,7 +1051,7 @@ function DataGridTableVirtual<TData extends object>({
// wrapper (any table state change recreates it), the layout props, and
// the column window. A scroll frame changes none of them, so the whole
// sortable-header subtree is reused instead of rebuilt per frame.
// eslint-disable-next-line react-hooks/exhaustive-deps
const headerNode = useMemo(
() =>
renderHeader && (
+10 -10
View File
@@ -28,7 +28,7 @@ import type {
DataGridTableInstance,
} from "@/components/reui/data-grid/data-grid"
import { flexRender, Subscribe } from "@tanstack/react-table"
import type { Cell, Column, Header, Row, Table } from "@tanstack/react-table"
import type { Cell, Column, Header, Row, } from "@tanstack/react-table"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -741,19 +741,19 @@ function DataGridTableBase({ children }: { children: ReactNode }) {
props.tableLayout?.columnsResizable,
// Visibility/order/pinning change the flat header set, so a column shown
// after mount must get its size variable even though sizing is untouched.
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnSizing,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnVisibility,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnOrder,
// eslint-disable-next-line react-hooks/exhaustive-deps
table.state.columnPinning,
// A def swap can change a column's `size` without touching sizing
// STATE; without this dep the CSS variables keep the old widths. For a
// consumer defining columns inline the memo degrades to per-render
// recompute, which is the safe direction.
// eslint-disable-next-line react-hooks/exhaustive-deps
table.options.columns,
])
@@ -956,7 +956,7 @@ function DataGridTableHead({ children }: { children: ReactNode }) {
function DataGridTableHeadRow({
children,
rowId,
rowId: _rowId,
}: {
children: ReactNode
rowId: string
@@ -1460,7 +1460,7 @@ function DataGridTableFootRowCell({
}
function DataGridTableBodyRowSkeleton({ children }: { children: ReactNode }) {
const { table, props } = useDataGrid()
const { props } = useDataGrid()
return (
<tr
@@ -2340,7 +2340,7 @@ const MemoizedDataGridTableBodyRows = memo(
) as typeof DataGridTableBodyRows
function DataGridTableHeader<TData extends object>() {
const { table, props } = useDataGrid()
const { table, props } = useDataGrid<TData>()
const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
@@ -2415,7 +2415,7 @@ function DataGridTable<TData extends object>({
footerContent?: ReactNode
renderHeader?: boolean
}) {
const { table, props } = useDataGrid()
const { table, props } = useDataGrid<TData>()
const mergedHeaderGroups = getDataGridTableMergedHeaderGroups(table)
const hasRightPinnedColumns = hasDataGridTableRightPinnedColumns(table)
+17 -81
View File
@@ -5,8 +5,9 @@ import {
useContext,
useEffect,
useId,
useMemo,
useLayoutEffect,
useRef,
useState,
} from "react"
import type { ReactNode } from "react"
import {
@@ -878,25 +879,17 @@ function DataGridProvider<TData extends object>({
table: DataGridTableInstance<TData>
children?: ReactNode
}) {
// Latest-props ref: context reads always resolve fresh props through the
// getter below without the memoized context value depending on unstable
// Stable for the grid's lifetime; ARIA ids and relations hang off it.
// Rendering reads this snapshot; imperative sizing reads committed values.
const gridId = useId()
// ReactNode/function prop identities (inline emptyMessage/onRowClick would
// otherwise publish a new context value on every consumer render - at
// mousemove rate during a resize drag, piercing the body-rows memo).
const propsRef = useRef(props)
propsRef.current = props
const i18n = mergeDataGridI18n(props.i18n)
const i18nRef = useRef(i18n)
i18nRef.current = i18n
// Same treatment for the table itself, which v9 - unlike v8 - re-creates on
// every state change. Depending on it directly would republish the context
// on each resize tick, which is exactly what the memo below exists to
// prevent; the getter still hands every consumer the current instance.
const tableRef = useRef(table)
tableRef.current = table
const [sizing] = useState(() => {
let committedTable = table
return {
controller: createDataGridAutoSizeController(() => committedTable),
update(next: DataGridTableInstance<TData>) { committedTable = next },
}
})
useLayoutEffect(() => { sizing.update(table) }, [sizing, table])
// Re-assert an explicit tableLayout resize mode so consumer-level useTable
// options cannot flip it back between drags. v9 makes `table.options`
@@ -936,70 +929,13 @@ function DataGridProvider<TData extends object>({
if (previousPageKeyRef.current === pageKey) return
previousPageKeyRef.current = pageKey
if (!cellSelectionOn || table.atoms.cellSelection == null) return
tableRef.current.resetCellSelection(true)
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [pageKey, cellSelectionOn])
table.resetCellSelection(true)
}, [pageKey, cellSelectionOn, table])
// One autoSize coordinator per table instance so split header/body viewports
// cannot apply the growth twice. Keyed on `table.store`, which v9 keeps
// stable for the life of the table, rather than on `table` itself: the
// wrapper is re-created on every state change, and re-creating the
// controller with it would reset its applied-once bookkeeping mid-drag.
const autoSize = useMemo(
() => createDataGridAutoSizeController(() => tableRef.current),
[table.store]
)
const tableState = table.state
const tableLayoutKey = JSON.stringify(props.tableLayout)
const tableClassNamesKey = JSON.stringify(props.tableClassNames)
// Memoize context value so consumers don't re-render during column resize.
// Column sizing state is intentionally excluded from deps -- CSS variables
// on the <table> element handle width updates without React re-renders.
// ReactNode/function props (messages, onRowClick) are also excluded: they
// are served fresh through the props getter, so unstable inline identities
// cannot invalidate the context value.
// `cellSelection` state is excluded on purpose too: a drag writes it once
// per cell crossed, and nothing reads it through context - cells and the
// selection bar subscribe to `table.atoms.cellSelection` directly.
const value = useMemo(
() => ({
get props() {
return propsRef.current
},
get table() {
return tableRef.current
},
get i18n() {
return i18nRef.current
},
recordCount: props.recordCount,
isLoading: props.isLoading || false,
gridId,
autoSize,
}),
// eslint-disable-next-line react-hooks/exhaustive-deps
[
autoSize,
props.recordCount,
props.isLoading,
props.loadingMode,
props.className,
tableLayoutKey,
tableClassNamesKey,
tableState.sorting,
tableState.pagination,
tableState.columnFilters,
tableState.rowSelection,
tableState.rowPinning,
tableState.expanded,
tableState.columnVisibility,
tableState.columnOrder,
tableState.columnPinning,
tableState.globalFilter,
]
)
const value = {
props, table, i18n, recordCount: props.recordCount,
isLoading: props.isLoading || false, gridId, autoSize: sizing.controller,
}
return (
// One React context serves every TData, but v9 declares both TFeatures and