From 336e4123e5905192ec56eee370cfaa62699560be Mon Sep 17 00:00:00 2001 From: PotapovaA Date: Wed, 19 Aug 2026 18:24:59 +0300 Subject: [PATCH] fix --- .../ColumnSelectionOverlay.jsx | 6 +- .../RealtimeTable/RealtimeTable.jsx | 297 +++++++++--------- .../SettingPanel/SettingPanel.jsx | 82 +++-- .../RealtimeTable/TableHead/TableHead.jsx | 201 ++++-------- 4 files changed, 259 insertions(+), 327 deletions(-) diff --git a/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx b/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx index 2470c03..1b27196 100644 --- a/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx +++ b/web/src/components/RealtimeTable/ColumnSelectionOverlay/ColumnSelectionOverlay.jsx @@ -1,4 +1,4 @@ -import { useEffect, useState } from 'react'; +import { memo, useEffect, useState } from 'react'; import { createPortal } from 'react-dom'; const toLocalCoord = (value, scale) => value / scale; @@ -54,7 +54,7 @@ const getOverlayRect = (container, overlayParent, selectedColumnId, columnPinnin return { top, left, width, height }; }; -export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => { +const ColumnSelectionOverlayComponent = ({ selectedColumnId, containerRef, scale = 1, columnSizing, columnPinning, showColumnFilters }) => { const [rect, setRect] = useState(null); const [portalTarget, setPortalTarget] = useState(null); @@ -135,3 +135,5 @@ export const ColumnSelectionOverlay = ({ selectedColumnId, containerRef, scale = portalTarget, ); }; + +export const ColumnSelectionOverlay = memo(ColumnSelectionOverlayComponent); diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index 73b574e..5e3ed64 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -29,7 +29,6 @@ import { additionExpenseRowTable, additionVspRowTable } from './constants/adding import { SelectVspModal } from './Modals/SelectVspModal'; import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal'; import { getRowId } from './utils/rowUtils'; -import { FORM_TYPE_OPTIONS } from '../../constants/constants'; import { debounce } from '@mui/material'; import { CreateProgramModal } from './Modals/CreateProgramProjectModal'; @@ -37,15 +36,69 @@ import { FormsNoCanAddRow } from './constants/formConfig'; import { DictVspApi } from '../../api/dict-vsp'; import { useAuth } from '../../app/context/AuthProvider'; +const CONFIG_CACHE = new Map(); + +const ROW_VIRTUALIZER_OPTIONS = { + overscan: 5, + scrollPaddingStart: 0, + scrollPaddingEnd: 0, + estimateSize: () => TABLE_ROW_HEIGHT, + measureElement: (element) => element?.offsetHeight || TABLE_ROW_HEIGHT, +}; + +const getColumnVirtualizerOptions = ({ table }) => { + const orderedVisibleColumns = [ + ...table.getLeftVisibleLeafColumns(), + ...table.getCenterVisibleLeafColumns(), + ...table.getRightVisibleLeafColumns(), + ]; + const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150; + + return { + overscan: 10, + estimateSize: getColumnSize, + measureElement: (element) => { + if (!element) return 150; + return getColumnSize(Number(element.getAttribute('data-index'))); + }, + }; +}; + +const hasVspDropdown = (columns) => + columns.some( + (column) => + column.editType === 'vsp_dropdown' || + (column.columns?.length && hasVspDropdown(column.columns)), + ); + +const tableContentStyle = { + position: 'relative', + width: '100%', + flex: 1, + minHeight: 0, + height: '100%', +}; + +const loadingOverlayStyle = { + position: 'absolute', + inset: 0, + display: 'flex', + alignItems: 'center', + justifyContent: 'center', + background: 'rgba(255, 255, 255, 0.6)', + zIndex: 2, +}; + const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const { user } = useAuth(); - const { data, setData, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); + const { data, columnsCurStage, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year); const [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumnId, setSelectedColumnId] = useState(); const [rowSelection, setRowSelection] = useState({}); - const [isLoadingData, setIsLoadingData] = useState(false); - const [isPending, startTransition] = useTransition(); + const rowSelectionRef = useRef(rowSelection); + rowSelectionRef.current = rowSelection; + const [, startTransition] = useTransition(); const [editingCell, setEditingCell] = useState(null); const [columnsConfig, setColumnsConfig] = useState(null); const { errors: validationErrors, isCellInvalid } = useValidationRules( @@ -60,13 +113,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const [isOpenModalSelectVsp, setIsOpenModalSelectVsp] = useState(false); const [isOpenModalSelectExpenseItem, setIsOpenModalSelectExpenseItem] = useState(false); - const [isOpenModalCreateProgram, setIsOpenModalCreateProgram] = useState(false); - const [isOpenModalCreateProject, setIsOpenModalCreateProject] = useState(false); - const [isProgramCreate, setIsProgramCreate] = useState(false) + const [createModalType, setCreateModalType] = useState(null); - const [isFormNoCanAddRow, setIsFormNoCanAddRow] = useState(() => { - return (FormsNoCanAddRow[formType].includes(sheetName)); - }) + const isFormNoCanAddRow = useMemo( + () => FormsNoCanAddRow[formType]?.includes(sheetName) ?? false, + [formType, sheetName], + ); const isVspAdditionRow = useMemo(() => { if (!formType || !sheetName) return false; @@ -80,21 +132,18 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { return additionExpenseRowTable[formType].includes(sheetName); }, [formType, sheetName]); - const handleGlobalFilterChange = useCallback( - debounce((value) => { + const handleGlobalFilterChange = useMemo( + () => debounce((value) => { startTransition(() => { setGlobalFilter(value); }); }, 300), - [] + [startTransition], ); + useEffect(() => () => handleGlobalFilterChange.clear(), [handleGlobalFilterChange]); + const { - isConnected, - isAuthenticated, - subscribeToErrors, - subscribeToAuthSuccess, - error: wsError, updateCell: contextUpdateCell, addRow: contextAddRow, deleteRow: contextDeleteRow, @@ -118,6 +167,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const { headerPortalRef, containerRef } = useHeaderPortal(); const rowVirtualizerRef = useRef(null); + const columnVirtualizerRef = useRef(null); const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(); @@ -126,21 +176,19 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { rowVirtualizerRef.current?.measure?.(); }, [sizeMult]); - const configCache = useRef(new Map()); - useEffect(() => { if (!formType || !sheetName) return; const cacheKey = `${formType}_${sheetName}`; - if (configCache.current.has(cacheKey)) { - setColumnsConfig(configCache.current.get(cacheKey)); + if (CONFIG_CACHE.has(cacheKey)) { + setColumnsConfig(CONFIG_CACHE.get(cacheKey)); return; } const loadConfig = async () => { try { const { config } = await import(`./constants/${formType}/${sheetName}.js`); - configCache.current.set(cacheKey, config.config); + CONFIG_CACHE.set(cacheKey, config.config); setColumnsConfig(config.config); } catch (error) { console.error(`Failed to load config for ${formType}/${sheetName}:`, error); @@ -154,14 +202,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { useEffect(() => { if (!formId || !columnsConfig?.columns) return; - const hasVspDropdown = (cols) => { - for (const col of cols) { - if (col.editType === 'vsp_dropdown') return true; - if (col.columns && hasVspDropdown(col.columns)) return true; - } - return false; - }; - if (!hasVspDropdown(columnsConfig.columns)) return; DictVspApi.getDropdownVsp({ form_id: formId }).then(data => { @@ -237,6 +277,28 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { isCellInvalid, ]); + // MRT запоминает индексы pinned-колонок до завершения асинхронной + // загрузки конфигурации. Новая ссылка на columnPinning заставляет + // виртуализатор пересчитать эти индексы уже с полным набором колонок. + useEffect(() => { + if (!columnsConfig?.columns?.length) return; + + setColumnPinning((currentPinning) => ({ + left: [...(currentPinning.left || [])], + right: [...(currentPinning.right || [])], + })); + }, [columnsConfig, setColumnPinning]); + + useEffect(() => { + if (!columns.length) return; + + const animationFrameId = requestAnimationFrame(() => { + columnVirtualizerRef.current?.measure?.(); + }); + + return () => cancelAnimationFrame(animationFrameId); + }, [columns, columnPinning, columnSizing, columnVisibility]); + const selectedColumnIdRef = useRef(selectedColumnId); useEffect(() => { @@ -248,14 +310,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { getSelectedColumnId: () => selectedColumnIdRef.current, }), [handleUpdateCell]); - const ROW_VIRTUALIZER_OPTIONS = { - overscan: 5, - scrollPaddingStart: 0, - scrollPaddingEnd: 0, - estimateSize: () => TABLE_ROW_HEIGHT, - measureElement: (el) => el?.offsetHeight || TABLE_ROW_HEIGHT, - }; - const createTableConfig = ({ columns, data, @@ -280,27 +334,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { enableRowVirtualization: true, enableColumnVirtualization: true, rowVirtualizerInstanceRef: rowVirtualizerRef, + columnVirtualizerInstanceRef: columnVirtualizerRef, rowVirtualizerOptions: ROW_VIRTUALIZER_OPTIONS, onEditingCellChange: (cell) => { if (cell && !editingCell) { contextStartEditing?.(cell.row, cell.column); } }, - columnVirtualizerOptions: ({ table }) => ({ - overscan: 10, - measureElement: (el) => { - if (!el) return 150; - const index = Number(el?.getAttribute?.('data-index')); - const isPinned = Boolean(el?.getAttribute?.('data-pinned')); - if (isPinned) { - const colId = table.getState().columnPinning.left[index]; - const column = table.getColumn(colId); - return column?.getSize() ?? 150; - } - const allCols = [...table.getLeftVisibleLeafColumns(), ...table.getCenterVisibleLeafColumns()] - return allCols[index]?.getSize() ?? 150; - }, - }), + columnVirtualizerOptions: getColumnVirtualizerOptions, enableRowSelection: true, onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, @@ -391,6 +432,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { ]); const table = useMaterialReactTable(tableConfig); + const isLoadingData = columns.length > 0; const handleNavigateToColumn = useCallback((columnId) => { if (!columnId) return; @@ -426,7 +468,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { } if (editingCell) return; - requestAnimationFrame(() => { + const animationFrameId = requestAnimationFrame(() => { try { const row = table.getRow(dataEditingCells.line_id); const cell = row?.getVisibleCells().find( @@ -437,88 +479,72 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { console.error(error); } }); + + return () => cancelAnimationFrame(animationFrameId); }, [dataEditingCells, editingCell, table]); - const [isFirstLoad, setIsFirstLoad] = useState(true); - - useEffect(() => { - if (isFirstLoad && table && data && columns.length !== 0) { - setIsFirstLoad(false); - setIsLoadingData(true); - } - }, [data, columns, table, isFirstLoad]); + const getSelectedRow = useCallback( + () => table.getRowModel().rows.find((row) => rowSelectionRef.current[row.id]), + [table], + ); const handleAddRow = useCallback(() => { - const allRows = table.getRowModel().rows; - - const selectedRows = allRows.filter(row => rowSelection[row.id]); - if (selectedRows.length === 0) { + const row = getSelectedRow(); + if (!row) { toast.error('Выделите строку для вставки'); return; } - const row = selectedRows[0]; - const expense_item_id = row.original.data.header.expense_item_id; - contextAddRow({ expense_item_id: expense_item_id }) - }, [rowSelection, table]) + contextAddRow({ expense_item_id: row.original.data.header.expense_item_id }); + }, [contextAddRow, getSelectedRow]); const handleAddVspRow = useCallback((vsp_id) => { - contextAddRow({ vsp_id: vsp_id }); - }, []); + contextAddRow({ vsp_id }); + }, [contextAddRow]); const handleAddExpenseItemRow = useCallback((expense_item) => { - const allRows = table.getRowModel().rows; - const selectedRows = allRows.filter(row => rowSelection[row.id]); - const row = selectedRows[0]; + const row = getSelectedRow(); + if (!row) return; contextAddRow({ expense_item_id: expense_item.id, project_id: row.original.data.header.project_id }); - }, [rowSelection]); + }, [contextAddRow, getSelectedRow]); const handleAddProgramRow = useCallback((name) => { - contextAddProgram({ name: name }); - }, []); + contextAddProgram({ name }); + }, [contextAddProgram]); const handleAddProjectRow = useCallback((name) => { - const allRows = table.getRowModel().rows; - const selectedRows = allRows.filter(row => rowSelection[row.id]); - const row = selectedRows[0]; - contextAddProject({ name: name, program_id: row.original.data.header.program_id }); - }, [rowSelection]); + const row = getSelectedRow(); + if (!row) return; + contextAddProject({ name, program_id: row.original.data.header.program_id }); + }, [contextAddProject, getSelectedRow]); const handleOpenModalSelectVsp = useCallback(() => { setIsOpenModalSelectVsp(true); - }, []) + }, []); const handleOpenModalSelectExpenseItem = useCallback(() => { - const allRows = table.getRowModel().rows; - - const selectedRows = allRows.filter(row => rowSelection[row.id]); - if (selectedRows.length === 0) { + const row = getSelectedRow(); + if (!row) { toast.error('Выделите строку для вставки'); return; } - const row = selectedRows[0]; if (row.original.row_type !== 'ITEM') { toast.error('Выделите строку проекта'); return; } setIsOpenModalSelectExpenseItem(true); - }, [rowSelection]) + }, [getSelectedRow]); const handleDeleteRow = useCallback(() => { - const allRows = table.getRowModel().rows; - - const selectedRows = allRows.filter(row => rowSelection[row.id]); - if (selectedRows.length === 0) { + const row = getSelectedRow(); + if (!row) { toast.error('Выделите строку для удаления'); return; } - const row = selectedRows[0]; - const rowId = getRowId(row); - contextDeleteRow(rowId); + contextDeleteRow(getRowId(row)); setRowSelection({}); - }, [rowSelection, table]) + }, [contextDeleteRow, getSelectedRow]); const addRow = useCallback(() => { - if (isVspAdditionRow) { return handleOpenModalSelectVsp(); } @@ -529,35 +555,30 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { }, [isVspAdditionRow, isAdditionExpenseItem, handleOpenModalSelectVsp, handleAddRow, handleOpenModalSelectExpenseItem]); const addProgram = useCallback(() => { - setIsProgramCreate(true); - setIsOpenModalCreateProgram(true); - }, []) + setCreateModalType('program'); + }, []); const addProject = useCallback(() => { - setIsProgramCreate(false); - const allRows = table.getRowModel().rows; - - const selectedRows = allRows.filter(row => rowSelection[row.id]); - if (selectedRows.length === 0) { + const row = getSelectedRow(); + if (!row) { toast.error('Выделите строку для вставки'); return; } - const row = selectedRows[0]; if (row.original.row_type !== 'GROUP') { toast.error('Выделите строку программы'); return; } - setIsOpenModalCreateProgram(true); - }, [setIsOpenModalCreateProgram, rowSelection]) + setCreateModalType('project'); + }, [getSelectedRow]); + + const closeSelectVspModal = useCallback(() => setIsOpenModalSelectVsp(false), []); + const closeSelectExpenseItemModal = useCallback(() => setIsOpenModalSelectExpenseItem(false), []); + const closeCreateModal = useCallback(() => setCreateModalType(null), []); useEffect(() => { return () => { - setData([]); - setRowSelection({}); - setEditingCell(null); - if (rowVirtualizerRef.current) { - rowVirtualizerRef.current = null; - } + rowVirtualizerRef.current = null; + columnVirtualizerRef.current = null; }; }, []); @@ -567,7 +588,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { if (newState) { const allRows = table.getRowModel().flatRows; - console.log(allRows); const newExpanded = {}; for (const row of allRows) { @@ -603,6 +623,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { onGlobalFilterChange={handleGlobalFilterChange} sizeMult={sizeMult} onChangeShowColumnFilters={setShowColumnFilters} + showColumnFilters={showColumnFilters} onAddRow={addRow} onAddProgram={addProgram} onAddProject={addProject} @@ -622,15 +643,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
-
+
{ /> {isTableLoading && ( -
+
)} @@ -662,7 +665,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { createPortal( theme.zIndex.modal - 1 }} + columns={columns} + columnPinning={columnPinning} + columnSizing={columnSizing} + columnVisibility={columnVisibility} + showColumnFilters={showColumnFilters} selectedColumnId={selectedColumnId} onColumnSelect={handleColumnSelect} onChangeWidth={handleSetColumnWidth} @@ -675,24 +682,24 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setIsOpenModalSelectVsp(false)} + onClose={closeSelectVspModal} formId={formId} onSelect={handleAddVspRow} title="Выбор ВСП" /> setIsOpenModalSelectExpenseItem(false)} + onClose={closeSelectExpenseItemModal} formId={formId} onSelect={handleAddExpenseItemRow} title="Добавление строки" sheet={sheetName} /> setIsOpenModalCreateProgram(false)} - title={isProgramCreate ? "Создание программы" : "Создание проекта"} - onCreate={isProgramCreate ? handleAddProgramRow : handleAddProjectRow} + isOpen={createModalType !== null} + onClose={closeCreateModal} + title={createModalType === 'program' ? 'Создание программы' : 'Создание проекта'} + onCreate={createModalType === 'program' ? handleAddProgramRow : handleAddProjectRow} /> ); diff --git a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx index fde2759..1851c34 100644 --- a/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx +++ b/web/src/components/RealtimeTable/SettingPanel/SettingPanel.jsx @@ -1,5 +1,5 @@ import { Divider, IconButton, Stack, Tooltip } from '@mui/material'; -import { useCallback, useEffect, useMemo, useState } from 'react'; +import { memo, useCallback, useMemo, useState } from 'react'; import { FORM_TYPE_OPTIONS } from '../../../constants/constants'; import { exportSheet, exportSheetProject } from '../../../utils/exportFile'; import { ExportDefaultButton } from '../../common/Buttons/ButtonsActions'; @@ -31,6 +31,9 @@ const controlSx = { flex: '0 0 2.5rem', }; +const searchSx = { height: '2.5rem' }; +const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' }; + const SettingPanel = ({ selectedColumnId, columnPinning, @@ -45,6 +48,7 @@ const SettingPanel = ({ onGlobalFilterChange, sizeMult, onChangeShowColumnFilters, + showColumnFilters, onAddRow, onAddProgram, onAddProject, @@ -61,49 +65,43 @@ const SettingPanel = ({ showOnlyDepth2, onToggleDepthVisibility, }) => { - const [isPinned, setIsPinned] = useState(false); const [isExporting, setIsExporting] = useState(false); - const [selectedColumnIds, setSelectedColumnIds] = useState(); - const [showColumnFilters, setShowColumnFilters] = useState(false); - - useEffect(() => { - if (!selectedColumnId) { - setIsPinned(false); - return; - } - const pinningColumn = columnPinning?.left || table.getState().columnPinning.left || []; - const pinned = pinningColumn.includes(selectedColumnId); - setIsPinned(pinned); - }, [selectedColumnId, columnPinning, table]); - - useEffect(() => { - setSelectedColumnIds(Object.keys(columnVisibility).filter((key) => !columnVisibility[key])); - }, [columnVisibility]); + const isPinned = useMemo( + () => Boolean(selectedColumnId && columnPinning?.left?.includes(selectedColumnId)), + [columnPinning, selectedColumnId], + ); + const selectedColumnIds = useMemo( + () => Object.keys(columnVisibility).filter((key) => !columnVisibility[key]), + [columnVisibility], + ); //TO DO: сделать const { addColorForCells } = {}; - const handleChangeShowColumnFilters = () => { - const show = table.getState().showColumnFilters; - setShowColumnFilters(!show); - onChangeShowColumnFilters(!show); - }; + const handleChangeShowColumnFilters = useCallback(() => { + onChangeShowColumnFilters(!showColumnFilters); + }, [onChangeShowColumnFilters, showColumnFilters]); - const handleExport = async () => { + const handleExport = useCallback(async () => { if (isExporting) return; - if (formType === 'PROJECT') { - if (!formId || !sheetName || !year) return; - setIsExporting(true); - await exportSheetProject(formId, sheetName, year); - setIsExporting(false); - return; - } + if (!formId || !sheetName || (formType === 'PROJECT' && !year)) return; - if (!formId || !sheetName) return; setIsExporting(true); - await exportSheet(formId, sheetName, direction); - setIsExporting(false); - }; + try { + if (formType === 'PROJECT') { + await exportSheetProject(formId, sheetName, year); + } else { + await exportSheet(formId, sheetName, direction); + } + } finally { + setIsExporting(false); + } + }, [direction, formId, formType, isExporting, sheetName, year]); + + const handleTogglePin = useCallback(() => { + if (!selectedColumnId) return; + isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId); + }, [isPinned, onPinColumn, onUnpinColumn, selectedColumnId]); const leafColumns = useMemo(() => collectLeafColumns({ columns }), [columns]); const stageColumnIds = useMemo( () => new Set([...stageColumnsHiddenFromPicker, ...(columnsCurStage || [])]), @@ -140,13 +138,7 @@ const SettingPanel = ({ data-active={isPinned} disabled={!selectedColumnId} onMouseDown={(e) => e.stopPropagation()} - onClick={() => { - if (!selectedColumnId) { - console.warn('[ColumnPin] клик Pin: selectedColumnId пустой'); - return; - } - isPinned ? onUnpinColumn?.(selectedColumnId) : onPinColumn?.(selectedColumnId); - }}> + onClick={handleTogglePin}> @@ -223,7 +215,7 @@ const SettingPanel = ({ - + @@ -253,4 +245,4 @@ const SettingPanel = ({ ); }; -export default SettingPanel; +export default memo(SettingPanel); diff --git a/web/src/components/RealtimeTable/TableHead/TableHead.jsx b/web/src/components/RealtimeTable/TableHead/TableHead.jsx index 189702c..9910a5e 100644 --- a/web/src/components/RealtimeTable/TableHead/TableHead.jsx +++ b/web/src/components/RealtimeTable/TableHead/TableHead.jsx @@ -1,75 +1,22 @@ import React, { useMemo } from 'react'; -import { columnPinningDefault } from '../constants/columnConfig'; import ColumnResizer from './ColumnResizer'; -const getPinnedColumnIds = (table) => (table.getState().columnPinning.left || []).filter(Boolean); - const getSortedColumns = (table) => { - const allLeafColumns = table.getAllLeafColumns(); - const pinnedColumns = getPinnedColumnIds(table); - - return [...allLeafColumns].sort((a, b) => { - const aIsPinned = pinnedColumns.includes(a.id); - const bIsPinned = pinnedColumns.includes(b.id); - - if (aIsPinned && bIsPinned) { - return pinnedColumns.indexOf(a.id) - pinnedColumns.indexOf(b.id); - } - if (aIsPinned) return -1; - if (bIsPinned) return 1; - return 0; - }); + return [ + ...table.getLeftVisibleLeafColumns(), + ...table.getCenterVisibleLeafColumns(), + ...table.getRightVisibleLeafColumns(), + ]; }; -const getColumnId = (header) => { - //в header.id содержаться колонки по типу depth_idColParent_idChild, а в pinningColumn только idCol, которые закреплены - //header.id.split('_').at(-1) - получение самой нижней колонки - let columnId = header?.id?.match(/data\..*$/)?.[0] || header?.id || header; - columnPinningDefault.left.forEach((colPinned) => { - if (columnId.includes(colPinned)) { - columnId = colPinned; - } - }); - return columnId; -}; +const getColumnPinnedStyles = (column) => { + const pinningPosition = column.getIsPinned(); -// Ширина закреплённых дочерних колонок группового заголовка -const getPinnedColumnsWidth = (header, pinningColumn) => { - let width = 0; - for (const leaf of header.getLeafHeaders()) { - if (pinningColumn.includes(leaf.column.id)) { - width += leaf.column.getSize(); - } else { - break; - } - } - return width; -}; - -const calculateLeftOffset = (table, columnOrHeader) => { - const columnId = getColumnId(columnOrHeader); - const pinnedColumns = getPinnedColumnIds(table); - const currentPinnedIndex = pinnedColumns.indexOf(columnId); - - let leftOffset = 0; - - if (currentPinnedIndex > 0) { - for (let i = 0; i < currentPinnedIndex; i++) { - const prevColumn = table.getColumn(pinnedColumns[i]); - if (prevColumn) { - leftOffset += prevColumn.getSize(); - } - } - } - - return leftOffset; -}; - -const isColumnPin = (table, header) => { - const pinningColumn = getPinnedColumnIds(table); - const columnId = getColumnId(header); - const isPinned = pinningColumn.includes(columnId); - return isPinned; + return { + isPinned: Boolean(pinningPosition), + left: pinningPosition === 'left' ? column.getStart('left') : undefined, + right: pinningPosition === 'right' ? column.getAfter('right') : undefined, + }; }; const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => { @@ -102,10 +49,9 @@ const ColumnNumbersRow = ({ table, selectedColumnId, onColumnSelect }) => { }; const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) => { - const isPinned = isColumnPin(table, column); + const { isPinned, left, right } = getColumnPinnedStyles(column); const allLeafColumns = table.getAllLeafColumns(); const originalIndex = allLeafColumns.findIndex((col) => col.id === column.id); - const leftOffset = calculateLeftOffset(table, column); const selected = selectedColumnId === column.id; return ( @@ -133,7 +79,8 @@ const ColumnNumberCell = ({ column, table, selectedColumnId, onColumnSelect }) = border: '.063rem solid rgba(209, 213, 220, 1)', borderBottom: 'none', position: isPinned ? 'sticky' : 'relative', - left: isPinned ? leftOffset : 'auto', + left, + right, zIndex: selected ? 16 : isPinned ? 20 : 'auto', cursor: 'pointer', userSelect: 'none', @@ -161,15 +108,15 @@ const getColorBrightness = (hexColor) => { return 0.299 * r + 0.587 * g + 0.114 * b; }; -const HeaderCell = ({ header, table, onClick, onChangeWidth }) => { +const HeaderCell = ({ header, table, pinningPosition, onClick, onChangeWidth }) => { const column = header.column; - const pinningColumn = getPinnedColumnIds(table); - const isGroup = header.subHeaders?.length > 0; - const isPinned = isColumnPin(table, header); - const leftOffset = calculateLeftOffset(table, header); const headerSize = header.getSize(); - const pinnedBandWidth = isGroup ? getPinnedColumnsWidth(header, pinningColumn) : 0; - const splitGroup = isGroup && isPinned && pinnedBandWidth > 0 && pinnedBandWidth < headerSize; + const isPinned = Boolean(pinningPosition); + const leftOffset = pinningPosition === 'left' ? header.getStart() : undefined; + const rightOffset = + pinningPosition === 'right' + ? table.getRightTotalSize() - header.getStart() - headerSize + : undefined; const customBgColor = column.columnDef?.muiTableHeadCellProps?.sx?.backgroundColor; @@ -185,34 +132,24 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => { onChangeWidth(header, size + deltaWidth); }; - // Вычисляем отступ для текста внутри ячейки - // Если колонка закреплена, текст должен быть привязан к левому краю ячейки - // Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок - const getTextLeftOffset = () => { - if (isPinned) { - return '0'; - } - // Для незакрепленных колонок текст должен начинаться после всех закрепленных - // Получаем общую ширину всех закрепленных колонок - const pinnedColumns = getPinnedColumnIds(table); - let totalPinnedWidth = 0; - for (const pinnedId of pinnedColumns) { - const pinnedCol = table.getColumn(pinnedId); - if (pinnedCol) { - totalPinnedWidth += pinnedCol.getSize(); - } - } - return `${totalPinnedWidth}px`; - }; - - const renderCell = (width, textLeft) => ( -
+ return ( +
onClick(header)} style={{ - width: width + 'px', - minWidth: width + 'px', - maxWidth: width + 'px', + width: '100%', + minWidth: '100%', + maxWidth: '100%', position: 'relative', padding: '4px 6px', textAlign: 'center', @@ -231,9 +168,7 @@ const HeaderCell = ({ header, table, onClick, onChangeWidth }) => {
{
); - - if (splitGroup) { - return ( - <> -
{renderCell(pinnedBandWidth, '0')}
-
{renderCell(headerSize - pinnedBandWidth, getTextLeftOffset(true))}
- - ); - } - - return ( -
- {renderCell(headerSize, getTextLeftOffset())} -
- ); }; const FilterRow = ({ table }) => { @@ -293,8 +208,7 @@ const FilterRow = ({ table }) => { // Компонент ячейки фильтра const FilterCell = ({ column, table }) => { - const isPinned = isColumnPin(table, column); - const leftOffset = calculateLeftOffset(table, column); + const { isPinned, left, right } = getColumnPinnedStyles(column); // Определяем тип фильтра на основе колонки const filterVariant = column.columnDef.filterVariant || 'text'; @@ -396,7 +310,8 @@ const FilterCell = ({ column, table }) => { border: '.063rem solid rgba(209, 213, 220, 1)', borderTop: 'none', position: isPinned ? 'sticky' : 'relative', - left: isPinned ? leftOffset : 'auto', + left, + right, zIndex: isPinned ? 19 : 'auto', }}> {renderFilterInput()} @@ -404,7 +319,7 @@ const FilterCell = ({ column, table }) => { ); }; -export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => { +const TableHeadComponent = ({ table, selectedColumnId, onColumnSelect, onChangeWidth, isLoadingData }) => { const handleHeaderClick = (header) => { if (!header.subHeaders?.length && header.column?.id) { onColumnSelect?.(header.column.id); @@ -415,6 +330,13 @@ export const TableHead = ({ table, selectedColumnId, onColumnSelect, onChangeWid return
; } + const headerSections = [ + { groups: table.getLeftHeaderGroups(), pinningPosition: 'left' }, + { groups: table.getCenterHeaderGroups(), pinningPosition: undefined }, + { groups: table.getRightHeaderGroups(), pinningPosition: 'right' }, + ]; + const headerRowsCount = Math.max(...headerSections.map(({ groups }) => groups.length)); + return ( <>
} - {/* Существующие строки заголовков */} - {table.getHeaderGroups().map((headerGroup) => ( -
- {headerGroup.headers.map((header) => ( - - - - ))} + {/* Pinned и обычные заголовки строятся отдельными группами. */} + {Array.from({ length: headerRowsCount }, (_, rowIndex) => ( +
+ {headerSections.flatMap(({ groups, pinningPosition }) => + (groups[rowIndex]?.headers || []).map((header) => ( + + )), + )}
))}
); }; + +export const TableHead = React.memo(TableHeadComponent);