исправила иерархию, блокировка ячеек, виртуализация строк

This commit is contained in:
PotapovaA 2026-05-29 18:38:05 +03:00
parent fcece07c64
commit 56833f454b
10 changed files with 111 additions and 96 deletions

View File

@ -1,7 +1,8 @@
import React from 'react';
import React, { useEffect, useState } from 'react';
import { useMemo } from 'react';
import styled from '@emotion/styled';
import { useRealtime } from '../../contexts/RealtimeContext';
import { toast } from 'react-toastify';
const ContainerForText = styled.span`
@ -42,7 +43,7 @@ const highlightText = (text, searchQuery) => {
);
};
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color }) => {
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
const cellValue = cell.getValue();
const textValue = String(cellValue || '');
@ -51,14 +52,11 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
return highlightText(textValue, globalFilter);
}, [textValue, globalFilter]);
const {
isCellLocked,
getCellLockInfo,
updateCell,
subscribeToErrors,
tryLockCell,
unlockCell
} = useRealtime();
const lockedStyles = !isEditable ? {
border: '2px solid #d3d3d3',
color: '#525252',
cursor: 'not-allowed',
} : {};
return (
@ -80,6 +78,7 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
padding: '8px',
backgroundColor: backgroundColor,
color: color,
...lockedStyles
}}
onClick={onClick}
>

View File

@ -7,7 +7,7 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
const left = tdFrag.offsetLeft;
const width = refCell.current.offsetParent.offsetWidth;
const translateY = 'translateY(' + trFrag.offsetTop + 'px)';
const translateY = trFrag.style.transform;
function useAutoResize(value) {
const ref = useRef(null);
@ -71,7 +71,7 @@ const EditCell = ({ refCell, onChange, disabled, value }) => {
}}
onChange={handleChangeValue}
value={curValue}
onBlur={handleChange}
onBlur={handleChange}
/>
</div>
</td>

View File

@ -1,5 +1,5 @@
// components/RealtimeTable/index.js
import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react';
import React, { useState, useMemo, useRef, useEffect, useCallback, useTransition } from 'react';
import { createPortal } from 'react-dom';
import {
useMaterialReactTable,
@ -25,12 +25,18 @@ import { toast } from 'react-toastify';
const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { data, setData } = useRealtimeData(formId, sheetName, direction);
const [isLoading, setIsLoading] = useState(true);
const [globalFilter, setGlobalFilter] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false);
const [selectedColumn, setSelectedColumn] = useState();
const columnVirtualizerInstanceRef = useRef(null);
const [rowSelection, setRowSelection] = useState({});
const [isLoadingData, setIsLoadingData] = useState(false);
const [isPending, startTransition] = useTransition();
const handleGlobalFilterChange = (value) => {
startTransition(() => {
setGlobalFilter(value);
});
};
const {
isConnected,
@ -85,7 +91,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
setRowSelection((prev) => ({
[rowId]: !prev[rowId],
}));
}, [rowSelection])
}, [])
const columns = useMemo(() => {
@ -108,7 +114,6 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
}
}, [columnsConfig, handleUpdateCell]);
const tableMeta = useMemo(() => ({
updateCell: handleUpdateCell,
}), [handleUpdateCell]);
@ -119,13 +124,17 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
...BASE_TABLE_CONFIG,
columns,
data: data,
columnVirtualizerInstanceRef,
enableRowVirtualization: false,
enableRowVirtualization: true,
rowVirtualizerOptions: {
overscan: 50,
scrollPaddingStart: 0,
scrollPaddingEnd: 0,
},
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onColumnSizingChange: setColumnSizing,
onColumnPinningChange: setColumnPinning,
onGlobalFilterChange: setGlobalFilter,
onGlobalFilterChange: handleGlobalFilterChange,
state: {
columnSizing,
columnPinning,
@ -135,7 +144,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
rowSelection
},
initialState: {
hiddenColumn: []
expanded: true
},
meta: tableMeta,
muiTableHeadCellProps: ({ column }) => ({
@ -153,22 +162,13 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
height: '1px',
minHeight: '1px',
maxHeight: '1px',
opacity: 0,
pointerEvents: 'none',
backgroundColor: 'transparent',
'& th': {
height: '1px',
minHeight: '1px',
maxHeight: '1px',
},
visibility: 'hidden',
},
},
muiTablePaperProps: getTablePaperStyles(),
muiTableContainerProps: {
ref: containerRef,
sx: {
overflowX: 'auto',
overflowY: 'auto',
position: 'relative',
contain: 'layout',
minHeight: '100%',
@ -198,6 +198,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const table = useMaterialReactTable(tableConfig);
useEffect(() => {
if (table && data && columns.length !== 0) {
setIsLoadingData(true)
}
}, [data, columns, table])
const handleAddRow = useCallback(() => {
const allRows = table.getRowModel().rows;
@ -222,7 +228,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const row = selectedRows[0];
const expense_item_id = row.original.data.line_id;
contextDeleteRow(expense_item_id)
setRowSelection({});
}, [rowSelection, table])
if (!data) {
return <div>Загрузка...</div>;
}
@ -237,11 +245,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
onToggleColumnVisibility={handleToggleColumnVisibility}
columnVisibility={columnVisibility}
onChangeSizeMult={setSizeMult}
onGlobalFilterChange={setGlobalFilter}
onGlobalFilterChange={handleGlobalFilterChange}
sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters}
onAddRow={handleAddRow}
onDeleteRow={handleDeleteRow}
isLoadingData={isLoadingData}
/>
<div style={tableWrapperStyle}>
@ -265,6 +274,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
onHeaderSelect={setSelectedColumn}
selectHeader={selectedColumn}
onChangeWidth={handleSetColumnWidth}
isLoadingData={isLoadingData}
/>,
headerPortalRef.current,
)}

View File

@ -31,6 +31,7 @@ const SettingPanel = ({
onChangeShowColumnFilters,
onAddRow,
onDeleteRow,
isLoadingData,
}) => {
const [isPinned, setIsPinned] = useState(false);
const [columnTree, setColumnTree] = useState();
@ -41,7 +42,7 @@ const SettingPanel = ({
if (!table) return;
setColumnTree(table.getAllColumns());
setShowColumnFilters(table.getState().showColumnFilters);
}, [table]);
}, [table, isLoadingData]);
useEffect(() => {
if (!selectedColumn) return;

View File

@ -131,7 +131,6 @@ const HeaderCell = ({ header, table, onClick, hightlight, onChangeWidth }) => {
// Если колонка не закреплена, текст должен начинаться после всех закрепленных колонок
const getTextLeftOffset = () => {
if (isPinned) {
// Для закрепленных колонок текст остается на месте
return '0';
}
// Для незакрепленных колонок текст должен начинаться после всех закрепленных
@ -215,7 +214,7 @@ const FilterRow = ({ table }) => {
style={{
display: 'flex',
position: 'sticky',
top: '1.5rem', // После строки с номерами колонок
top: '1.5rem',
zIndex: 20,
backgroundColor: 'white',
}}
@ -338,11 +337,16 @@ export const TableHead = ({
onHeaderSelect,
selectHeader,
onChangeWidth,
isLoadingData,
}) => {
const handleHeaderClick = (header) => {
onHeaderSelect(header);
};
if (!isLoadingData) {
return <div></div>
}
return (
<>
<div

View File

@ -86,15 +86,8 @@ const useRealtimeData = (formId, sheetName, direction) => {
return;
}
const maxDepth = data.reduce(
(prev, current) => {
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
},
{ depth: 0 },
).depth;
const formattedData = formatedToSubRows(maxDepth, data);
setData(formattedData);
const hierarchicalData = buildHierarchy(data);
setData(hierarchicalData);
} catch (err) {
console.error("Failed to load initial data:", err);
setError(err.message);
@ -104,7 +97,54 @@ const useRealtimeData = (formId, sheetName, direction) => {
};
loadInitialData();
}, [formId, sheetName, formatedToSubRows]);
}, [formId, sheetName]);
const buildHierarchy = (items) => {
const result = [];
const stack = [];
const getTypeLevel = (type) => {
const levels = {
ROOT: 0,
GROUP: 1,
ITEM: 2,
SUB_ITEM: 3,
INPUT: 4,
};
return levels[type] ?? 999;
};
for (const item of items) {
const currentLevel = getTypeLevel(item.row_type);
while (
stack.length > 0 &&
getTypeLevel(stack[stack.length - 1].row_type) >= currentLevel
) {
stack.pop();
}
const parent = stack.length > 0 ? stack[stack.length - 1] : null;
const newNode = { ...item, subRows: [] };
if (parent) {
if (!parent.subRows) {
parent.subRows = [];
}
parent.subRows.push(newNode);
} else {
result.push(newNode);
}
// Добавляем текущий узел в стек, если он может иметь детей
if (item.row_type !== "INPUT") {
stack.push(newNode);
}
}
return result;
};
useEffect(() => {
setIsConnected(wsConnected);

View File

@ -9,7 +9,8 @@ function EditCellPortal({
EditCell,
onSaveStart,
onSaveEnd,
onError
onError,
isEditable
}) {
const ref = useRef(null);
const [refTbody, setRefTbody] = useState(false);
@ -25,17 +26,7 @@ function EditCellPortal({
const handleChange = async (val) => {
table.setEditingCell(null);
if (isSaving) return;
setIsSaving(true);
onSaveStart?.();
// Сохраняем старое значение на случай ошибки
const oldValue = cell.getValue();
try {
// Используем updateCell из контекста (передается через meta)
if (table.options.meta?.updateCell) {
const success = await table.options.meta.updateCell(
cell.row,
@ -44,32 +35,15 @@ function EditCellPortal({
);
if (success) {
console.log('Cell updated successfully via WebSocket');
onSaveEnd?.(true);
table.setEditingCell(null);
} else {
console.error('Failed to update cell via WebSocket');
const errorMsg = 'Не удалось сохранить изменение';
onError?.(errorMsg);
onSaveEnd?.(false);
// Возвращаем предыдущее значение
}
} else {
// Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback)
console.warn('WebSocket updateCell not configured in meta');
if (table.options.meta?.onCellUpdate) {
table.options.meta.onCellUpdate(cell.row.id, cell.column.id, val);
}
onSaveEnd?.(true);
table.setEditingCell(null);
}
} catch (error) {
console.error('Error updating cell:', error);
onError?.(error.message);
onSaveEnd?.(false);
} finally {
setIsSaving(false);
}
};
@ -88,7 +62,7 @@ function EditCellPortal({
refCell={ref}
cell={cell}
value={cell.getValue()}
disabled={isDisabled || isSaving}
disabled={!isEditable}
onChange={handleChange}
/>,
refTbody,
@ -131,8 +105,9 @@ export const getTableColumns = ({
column={column}
cell={cell}
globalFilter={
table.getState().globalFilter || column.getFilterValue()
table.getState().globalFilter
}
isEditable={row.original?.row_type === 'INPUT' || false}
backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
color={columnColors[column.id]?.color_type?.['COLOR']}
isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]}
@ -144,20 +119,8 @@ export const getTableColumns = ({
cell={cell}
table={table}
EditCell={EditCell}
onSaveStart={() => {
if (table.options.meta?.setUpdatingCell) {
table.options.meta.setUpdatingCell(row.id, column.id, true);
}
}}
onSaveEnd={(success) => {
if (table.options.meta?.setUpdatingCell) {
table.options.meta.setUpdatingCell(row.id, column.id, false);
}
if (!success && onCellUpdateError) {
onCellUpdateError(row.id, column.id, 'Failed to save');
}
}}
onError={onCellUpdateError}
isEditable={row.original?.row_type === 'INPUT' || false}
/>
);
@ -177,10 +140,9 @@ export const getTableColumns = ({
enableEditing: false,
enablePinning: true,
Cell: ({ cell, row }) => {
return <span
onClick={() => {console.log('TEST', row.id); onCellNumberClick(row.id)}}
>{cell.getValue()}</span>;
return <button onClick={() => { console.log('TEST', row.id); onCellNumberClick(row.id) }}>
{cell.getValue()}
</button>
},
};

View File

@ -1,5 +1,4 @@
export function setNewValueToData(data, colId, rowId, value) {
// 1. Находим целевой объект по rowId(индексы вложенных subRows)
let targetObject = data;
if (rowId && rowId.length) {
const indices = rowId.split(".").map(Number);

View File

@ -14,7 +14,7 @@ const TableCard = ({ table, onNavigate }) => {
};
return (
<div key={table}>
<div key={table.sheet + '_' + table?.direction}>
<Section onClick={handleClick}>
<SectionStartPart>
<div className="for-img-table">

View File

@ -26,7 +26,7 @@ const TablesList = ({
return (
<TableCard
key={table.sheet}
key={table.sheet+"_"+table?.direction}
onNavigate={() => handleNavigate(path)}
table={table}
/>