chore(ui): сохранить основу поэтапной миграции таблиц

This commit is contained in:
Denozordec committed 2026-09-13 09:46:41 +07:00
1 parent c30f817926
commit e7755a4d16
100 files changed
+30178 -1089

No files matched your search

@@ -1,11 +1,14 @@
"use client"
import { HTMLAttributes, memo, ReactNode, useMemo } from "react"
import { memo, useMemo } from "react"
import type { HTMLAttributes, ReactNode } from "react"
import {
getColumnHeaderLabel,
useDataGrid,
} from "@/components/reui/data-grid/data-grid"
import { Column } from "@tanstack/react-table"
import type { DataGridFeatures } from "@/components/reui/data-grid/data-grid"
import { Subscribe } from "@tanstack/react-table"
import type { Column } from "@tanstack/react-table"
import { cn } from "@/lib/utils"
import { Button } from "@/components/ui/button"
@@ -25,19 +28,20 @@ import {
import { ArrowDownIcon, ArrowUpIcon, ChevronsUpDownIcon, CheckIcon, ArrowLeftToLineIcon, ArrowRightToLineIcon, ArrowLeftIcon, ArrowRightIcon, Settings2Icon, PinOffIcon } from "lucide-react"
interface DataGridColumnHeaderProps<
TData,
TData extends object,
TValue,
> extends HTMLAttributes<HTMLDivElement> {
column: Column<TData, TValue>
column: Column<DataGridFeatures, TData, TValue>
/** When omitted, uses `column.columnDef.meta.headerTitle`, then a string `columnDef.header`, then `column.id`. */
title?: string
icon?: ReactNode
/** Reserved; pin controls are gated by tableLayout.columnsPinnable + column.getCanPin(). */
pinnable?: boolean
filter?: ReactNode
visibility?: boolean
}
function DataGridColumnHeaderInner<TData, TValue>({
function DataGridColumnHeaderInner<TData extends object, TValue>({
column,
title,
icon,
@@ -45,11 +49,20 @@ function DataGridColumnHeaderInner<TData, TValue>({
filter,
visibility = false,
}: DataGridColumnHeaderProps<TData, TValue>) {
const { isLoading, table, props, recordCount } = useDataGrid()
const { i18n, isLoading, table, props } = useDataGrid()
const resolvedTitle = title ?? getColumnHeaderLabel(column)
const columnOrder = table.getState().columnOrder
const columnVisibilityKey = JSON.stringify(table.getState().columnVisibility)
// TanStack's columnOrder defaults to [] until a consumer seeds it; fall
// back to the definition order so Move Left/Right work out of the box.
const columnOrderState = table.state.columnOrder
const columnOrder =
columnOrderState.length > 0
? columnOrderState
: table.getAllLeafColumns().map((leafColumn) => leafColumn.id)
const columnVisibilityKey =
props.tableLayout?.columnsVisibility && visibility
? JSON.stringify(table.state.columnVisibility)
: ""
const isSorted = column.getIsSorted()
const isPinned = column.getIsPinned()
const canSort = column.getCanSort()
@@ -76,18 +89,18 @@ function DataGridColumnHeaderInner<TData, TValue>({
)
const headerButtonClassName = cn(
"text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground -ms-2 px-2 font-normal h-6 rounded-lg",
"text-secondary-foreground/80 hover:bg-secondary data-[state=open]:bg-secondary hover:text-foreground data-[state=open]:text-foreground px-2 font-normal h-6 rounded-lg",
className
)
const sortIcon =
canSort &&
(isSorted === "desc" ? (
<ArrowDownIcon className="size-3.25" />
<ArrowDownIcon className="size-3.25" aria-hidden="true" />
) : isSorted === "asc" ? (
<ArrowUpIcon className="size-3.25" />
<ArrowUpIcon className="size-3.25" aria-hidden="true" />
) : (
<ChevronsUpDownIcon className="mt-px size-3.25" />
<ChevronsUpDownIcon className="mt-px size-3.25" aria-hidden="true" />
))
const hasControls =
@@ -128,7 +141,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
disabled={!canSort}
>
<ArrowUpIcon className="size-3.5!" />
<span className="grow">Asc</span>
<span className="grow">{i18n.labels.sortAscending}</span>
{isSorted === "asc" && (
<CheckIcon className="text-primary size-4 opacity-100!" />
)}
@@ -145,7 +158,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
disabled={!canSort}
>
<ArrowDownIcon className="size-3.5!" />
<span className="grow">Desc</span>
<span className="grow">{i18n.labels.sortDescending}</span>
{isSorted === "desc" && (
<CheckIcon className="text-primary size-4 opacity-100!" />
)}
@@ -162,21 +175,21 @@ function DataGridColumnHeaderInner<TData, TValue>({
items.push(
<DropdownMenuItem
key="pin-left"
onClick={() => column.pin(isPinned === "left" ? false : "left")}
onClick={() => column.pin(isPinned === "start" ? false : "start")}
>
<ArrowLeftToLineIcon className="size-3.5!" aria-hidden="true" />
<span className="grow">Pin to left</span>
{isPinned === "left" && (
<span className="grow">{i18n.labels.pinColumnStart}</span>
{isPinned === "start" && (
<CheckIcon className="text-primary size-4 opacity-100!" />
)}
</DropdownMenuItem>,
<DropdownMenuItem
key="pin-right"
onClick={() => column.pin(isPinned === "right" ? false : "right")}
onClick={() => column.pin(isPinned === "end" ? false : "end")}
>
<ArrowRightToLineIcon className="size-3.5!" aria-hidden="true" />
<span className="grow">Pin to right</span>
{isPinned === "right" && (
<span className="grow">{i18n.labels.pinColumnEnd}</span>
{isPinned === "end" && (
<CheckIcon className="text-primary size-4 opacity-100!" />
)}
</DropdownMenuItem>
@@ -203,7 +216,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
disabled={!canMoveLeft || isPinned !== false}
>
<ArrowLeftIcon className="size-3.5!" aria-hidden="true" />
<span>Move to Left</span>
<span>{i18n.labels.moveColumnStart}</span>
</DropdownMenuItem>,
<DropdownMenuItem
key="move-right"
@@ -218,7 +231,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
disabled={!canMoveRight || isPinned !== false}
>
<ArrowRightIcon className="size-3.5!" aria-hidden="true" />
<span>Move to Right</span>
<span>{i18n.labels.moveColumnEnd}</span>
</DropdownMenuItem>
)
hasPreviousSection = true
@@ -233,7 +246,7 @@ function DataGridColumnHeaderInner<TData, TValue>({
<DropdownMenuSub key="visibility">
<DropdownMenuSubTrigger>
<Settings2Icon className="size-3.5!" />
<span>Columns</span>
<span>{i18n.labels.columnsMenu}</span>
</DropdownMenuSubTrigger>
<DropdownMenuSubContent side="right">
{table
@@ -278,14 +291,14 @@ function DataGridColumnHeaderInner<TData, TValue>({
if (hasControls) {
return (
<div className="flex h-full items-center justify-between gap-1.5">
<div className="-ms-2 flex h-full items-center justify-between gap-1.5">
<DropdownMenu>
<DropdownMenuTrigger
render={
<Button
variant="ghost"
className={headerButtonClassName}
disabled={isLoading || recordCount === 0}
disabled={isLoading}
>
{icon && icon}
{resolvedTitle}
@@ -301,10 +314,10 @@ function DataGridColumnHeaderInner<TData, TValue>({
<Button
size="icon-sm"
variant="ghost"
className="-me-1 size-7 rounded-md"
className="rounded-lg -me-1 size-7"
onClick={() => column.pin(false)}
aria-label={`Unpin ${resolvedTitle} column`}
title={`Unpin ${resolvedTitle} column`}
aria-label={i18n.labels.unpinColumn(resolvedTitle)}
title={i18n.labels.unpinColumn(resolvedTitle)}
>
<PinOffIcon className="size-3.5! opacity-50!" aria-hidden="true" />
</Button>
@@ -315,11 +328,11 @@ function DataGridColumnHeaderInner<TData, TValue>({
if (canSort || (props.tableLayout?.columnsResizable && canResize)) {
return (
<div className="flex h-full items-center">
<div className="-ms-2 flex h-full items-center">
<Button
variant="ghost"
className={headerButtonClassName}
disabled={isLoading || recordCount === 0}
disabled={isLoading}
onClick={handleSort}
>
{icon && icon}
@@ -338,8 +351,47 @@ function DataGridColumnHeaderInner<TData, TValue>({
)
}
const DataGridColumnHeader = memo(
DataGridColumnHeaderInner
) as typeof DataGridColumnHeaderInner
const DataGridColumnHeaderMemo = memo(DataGridColumnHeaderInner) as <
TData extends object,
TValue,
>(
props: DataGridColumnHeaderProps<TData, TValue> & {
/** Internal: the state slices the header re-renders on. Not part of the public API. */
subscribedState?: unknown
}
) => ReactNode
/**
* Sort and pin state reaches this header through builder calls on `column`
* (`getIsSorted()`, `getIsPinned()`), and `column` is a stable reference. That
* combination is the one v9's fresh-table-per-state-change does NOT cover:
* React Compiler is free to memoize against the stable column and never
* re-evaluate those reads, which shows up as frozen sort arrows and pin
* controls. The `Subscribe` below turns the slices this header actually reads
* into a real reactive dependency, and threading the selection through as a
* prop is what lets it past the `memo` - which would otherwise see unchanged
* props and skip the render anyway.
*/
function DataGridColumnHeader<TData extends object, TValue>(
props: DataGridColumnHeaderProps<TData, TValue>
) {
const { table } = useDataGrid()
return (
<Subscribe
source={table.store}
selector={(state) => ({
sorting: state.sorting,
columnPinning: state.columnPinning,
columnOrder: state.columnOrder,
columnVisibility: state.columnVisibility,
})}
>
{(subscribed) => (
<DataGridColumnHeaderMemo {...props} subscribedState={subscribed} />
)}
</Subscribe>
)
}
export { DataGridColumnHeader, type DataGridColumnHeaderProps }