diff --git a/apps/web/src/components/data-grid-card.tsx b/apps/web/src/components/data-grid-card.tsx index b198a45..bbb187e 100644 --- a/apps/web/src/components/data-grid-card.tsx +++ b/apps/web/src/components/data-grid-card.tsx @@ -9,10 +9,10 @@ import { type SortingState, type RowSelectionState, type VisibilityState, + type OnChangeFn, } from '@tanstack/react-table' import { Columns3Icon } from 'lucide-react' -import { Card, CardContent, CardHeader, CardTitle } from '@cfdm/ui/components/card' import { Checkbox } from '@cfdm/ui/components/checkbox' import { Button } from '@cfdm/ui/components/button' import { cn } from '@cfdm/ui/lib/utils' @@ -52,6 +52,24 @@ function loadStoredColumnVisibility(key: string): VisibilityState | undefined { } } +export { loadStoredColumnVisibility } + +export interface DataGridColumnVisibilityOption { + id: string + label: string +} + +export function dataTableColumnVisibilityOptions( + cols: DataTableColumn[], +): DataGridColumnVisibilityOption[] { + return cols + .filter((c) => c.enableHiding !== false) + .map((c) => ({ + id: c.key, + label: c.headerTitle ?? (typeof c.header === 'string' ? c.header : c.key), + })) +} + export interface DataGridCardProps { title?: ReactNode description?: ReactNode @@ -86,6 +104,11 @@ export interface DataGridCardProps { onRowSelectionChange?: (selectedIds: string[]) => void /** Показать picker видимости колонок. */ enableColumnVisibility?: boolean + /** Управляемая видимость колонок (для внешнего UI, напр. тулбар «Вид»). */ + columnVisibility?: VisibilityState + onColumnVisibilityChange?: OnChangeFn + /** Показать встроенную кнопку «Колонки». По умолчанию true при enableColumnVisibility. */ + columnVisibilityTrigger?: boolean /** Ключ localStorage для сохранения видимости колонок. */ columnVisibilityStorageKey?: string /** Начальная видимость колонок (перекрывает localStorage для отсутствующих ключей). */ @@ -93,6 +116,28 @@ export interface DataGridCardProps { className?: string } +function DataGridSectionHeader({ + title, + description, + actions, +}: { + title?: ReactNode + description?: ReactNode + actions?: ReactNode +}) { + if (!title && !description && !actions) return null + + return ( +
+
+ {title ?

{title}

: null} + {description ?

{description}

: null} +
+ {actions ?
{actions}
: null} +
+ ) +} + function DataGridPaginationBar() { return (
@@ -190,23 +235,32 @@ export function DataGridCard({ enableRowSelection = false, onRowSelectionChange, enableColumnVisibility = false, + columnVisibility: columnVisibilityProp, + onColumnVisibilityChange, + columnVisibilityTrigger, columnVisibilityStorageKey, initialColumnVisibility, className, }: DataGridCardProps) { const [sorting, setSorting] = useState(initialSorting ?? []) const [rowSelection, setRowSelection] = useState({}) - const [columnVisibility, setColumnVisibility] = useState(() => { + const [internalColumnVisibility, setInternalColumnVisibility] = useState(() => { const stored = columnVisibilityStorageKey ? loadStoredColumnVisibility(columnVisibilityStorageKey) : undefined return { ...initialColumnVisibility, ...stored } }) + const isColumnVisibilityControlled = columnVisibilityProp !== undefined + const columnVisibility = isColumnVisibilityControlled ? columnVisibilityProp : internalColumnVisibility + const setColumnVisibility: OnChangeFn = isColumnVisibilityControlled + ? (onColumnVisibilityChange ?? (() => undefined)) + : setInternalColumnVisibility + useEffect(() => { - if (!columnVisibilityStorageKey) return + if (isColumnVisibilityControlled || !columnVisibilityStorageKey) return localStorage.setItem(columnVisibilityStorageKey, JSON.stringify(columnVisibility)) - }, [columnVisibility, columnVisibilityStorageKey]) + }, [columnVisibility, columnVisibilityStorageKey, isColumnVisibilityControlled]) const selectColumn: ColumnDef = { id: 'select', @@ -274,11 +328,14 @@ export function DataGridCard({ enableHiding: enableColumnVisibility, }) - const columnVisibilityAction = enableColumnVisibility ? ( + const showColumnVisibilityTrigger = + enableColumnVisibility && (columnVisibilityTrigger ?? true) + + const columnVisibilityAction = showColumnVisibilityTrigger ? ( + @@ -286,37 +343,25 @@ export function DataGridCard({ /> ) : null - const headerActions = ( + const headerActions = actions ? (
{columnVisibilityAction} {actions}
+ ) : ( + columnVisibilityAction ) - const hasHeader = Boolean(title || description || actions || enableColumnVisibility) + const hasHeader = Boolean(title || description || actions || showColumnVisibilityTrigger) if (data.length === 0) { - if (!hasHeader) { - return ( -
- -
- ) - } - return ( - - -
- {title ? {title} : null} - {description ?

{description}

: null} -
- {headerActions ?
{headerActions}
: null} -
- - - -
+
+ {hasHeader ? ( + + ) : null} + +
) } @@ -335,30 +380,13 @@ export function DataGridCard({ /> ) - if (!hasHeader) { - return ( -
- {enableColumnVisibility ? ( -
{columnVisibilityAction}
- ) : null} - {gridBody} -
- ) - } - return ( - - -
- {title ? {title} : null} - {description ?

{description}

: null} -
- {(actions || enableColumnVisibility) ? ( -
{headerActions}
- ) : null} -
- {gridBody} -
+
+ {hasHeader ? ( + + ) : null} + {gridBody} +
) } diff --git a/apps/web/src/components/vps-filters-toolbar.tsx b/apps/web/src/components/vps-filters-toolbar.tsx index 5f78cf9..128d6c5 100644 --- a/apps/web/src/components/vps-filters-toolbar.tsx +++ b/apps/web/src/components/vps-filters-toolbar.tsx @@ -14,6 +14,8 @@ import { Slider } from '@cfdm/ui/components/slider' import { PlusIcon } from 'lucide-react' import { ListFiltersBar, type FilterChip } from '@/components/list-filters-bar' +import type { DataGridColumnVisibilityOption } from '@/components/data-grid-card' +import type { VisibilityState } from '@tanstack/react-table' import { Filters, @@ -47,6 +49,9 @@ interface VpsFiltersToolbarProps { projectNameOptions: string[] shownCount: number totalCount: number + columnVisibilityOptions?: DataGridColumnVisibilityOption[] + columnVisibility?: VisibilityState + onColumnVisibilityChange?: (columnId: string, visible: boolean) => void } const RU_I18N: FilterI18nConfig = { @@ -171,6 +176,9 @@ export function VpsFiltersToolbar({ projectNameOptions, shownCount, totalCount, + columnVisibilityOptions, + columnVisibility, + onColumnVisibilityChange, }: VpsFiltersToolbarProps) { const [presets, setPresets] = useState(() => loadFilterPresets()) @@ -519,6 +527,24 @@ export function VpsFiltersToolbar({
+ {columnVisibilityOptions && columnVisibilityOptions.length > 0 && onColumnVisibilityChange ? ( + <> + +
+ + {columnVisibilityOptions.map((col) => ( + + ))} +
+ + ) : null} +
diff --git a/apps/web/src/routes/_auth/vps.tsx b/apps/web/src/routes/_auth/vps.tsx index b49cd69..befbdca 100644 --- a/apps/web/src/routes/_auth/vps.tsx +++ b/apps/web/src/routes/_auth/vps.tsx @@ -11,7 +11,8 @@ import { PageShell } from '@/components/page-shell' import { PageHeader } from '@/components/page-header' import { Button } from '@cfdm/ui/components/button' import { Badge } from '@cfdm/ui/components/badge' -import { DataGridCard, columnDefFromDataTable } from '@/components/data-grid-card' +import { DataGridCard, columnDefFromDataTable, loadStoredColumnVisibility, dataTableColumnVisibilityOptions } from '@/components/data-grid-card' +import type { VisibilityState } from '@tanstack/react-table' import type { DataTableColumn } from '@/components/data-grid-types' import { dataGridCellStack, dataGridCellWithFlag } from '@/components/data-grid-cells' import { CountryFlag } from '@/components/country-flag' @@ -274,6 +275,30 @@ function VpsPage() { [customFieldDefs], ) + const [columnVisibility, setColumnVisibility] = useState(() => ({ + ...(loadStoredColumnVisibility('vps-column-visibility') ?? {}), + })) + + useEffect(() => { + setColumnVisibility((prev) => ({ ...customColumnVisibility, ...prev })) + }, [customColumnVisibility]) + + useEffect(() => { + localStorage.setItem('vps-column-visibility', JSON.stringify(columnVisibility)) + }, [columnVisibility]) + + const handleColumnVisibilityChange = (columnId: string, visible: boolean) => { + setColumnVisibility((prev) => { + const next = { ...prev } + if (visible) { + delete next[columnId] + } else { + next[columnId] = false + } + return next + }) + } + const columns: DataTableColumn[] = useMemo(() => { const base: DataTableColumn[] = [ { @@ -437,6 +462,11 @@ function VpsPage() { deleteMutation, ]) + const columnVisibilityOptions = useMemo( + () => dataTableColumnVisibilityOptions(columns), + [columns], + ) + return ( {tableSections.map((section) => ( 200} height={560} enableColumnVisibility - columnVisibilityStorageKey="vps-column-visibility" - initialColumnVisibility={customColumnVisibility} + columnVisibility={columnVisibility} + onColumnVisibilityChange={setColumnVisibility} + columnVisibilityTrigger={false} /> ))}