Merge pull request 'Оптимизация' (#46) from table-optimization into test
Reviewed-on: #46
This commit is contained in:
commit
e6133b82fe
@ -1,41 +1,67 @@
|
|||||||
import React, { useEffect, useState } from 'react';
|
import React, { memo, useMemo, useCallback } from 'react';
|
||||||
import { useMemo } from 'react';
|
|
||||||
import styled from '@emotion/styled';
|
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||||
import { toast } from 'react-toastify';
|
|
||||||
|
|
||||||
const ContainerForText = styled.span`
|
// Константы вне компонента
|
||||||
display: -webkit-box;
|
const BASE_CELL_STYLES = {
|
||||||
-webkit-box-orient: vertical;
|
width: '100%',
|
||||||
-webkit-line-clamp: 3;
|
height: '100%',
|
||||||
overflow: hidden;
|
display: 'flex',
|
||||||
white-space: pre-wrap;
|
alignItems: 'center',
|
||||||
`;
|
justifyContent: 'center',
|
||||||
|
boxSizing: 'border-box',
|
||||||
|
position: 'absolute',
|
||||||
|
top: 0,
|
||||||
|
left: 0,
|
||||||
|
right: 0,
|
||||||
|
bottom: 0,
|
||||||
|
border: '2px solid transparent',
|
||||||
|
padding: '8px',
|
||||||
|
};
|
||||||
|
|
||||||
// Стилизованный компонент для плашки блокировки
|
const LOCKED_STYLES = {
|
||||||
const LockBadge = styled.div`
|
border: '2px solid #e0e0e0',
|
||||||
position: absolute;
|
backgroundColor: '#f5f5f5',
|
||||||
top: 2px;
|
cursor: 'not-allowed',
|
||||||
right: 2px;
|
opacity: 0.85,
|
||||||
background: rgba(0, 0, 0, 0.7);
|
};
|
||||||
color: white;
|
|
||||||
font-size: 9px;
|
const LOCK_BADGE_STYLES = {
|
||||||
padding: 1px 4px;
|
position: 'absolute',
|
||||||
border-radius: 3px;
|
top: '2px',
|
||||||
pointer-events: none;
|
right: '2px',
|
||||||
z-index: 10;
|
background: 'rgba(0, 0, 0, 0.7)',
|
||||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
color: 'white',
|
||||||
letter-spacing: 0.3px;
|
fontSize: '9px',
|
||||||
backdrop-filter: blur(4px);
|
padding: '1px 4px',
|
||||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
borderRadius: '3px',
|
||||||
`;
|
pointerEvents: 'none',
|
||||||
|
zIndex: 10,
|
||||||
|
fontFamily: '-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif',
|
||||||
|
letterSpacing: '0.3px',
|
||||||
|
backdropFilter: 'blur(4px)',
|
||||||
|
border: '1px solid rgba(255, 255, 255, 0.1)',
|
||||||
|
};
|
||||||
|
|
||||||
|
const CONTAINER_STYLES = {
|
||||||
|
display: '-webkit-box',
|
||||||
|
WebkitBoxOrient: 'vertical',
|
||||||
|
WebkitLineClamp: 3,
|
||||||
|
overflow: 'hidden',
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
wordBreak: 'break-word',
|
||||||
|
};
|
||||||
|
|
||||||
|
const INTEGER_COLUMN_IDS = new Set([
|
||||||
|
'data.header.num_group',
|
||||||
|
'data.nomenclature_group_id',
|
||||||
|
'data.header.item_id',
|
||||||
|
'data.article_id',
|
||||||
|
'data.code_razdela',
|
||||||
|
]);
|
||||||
|
|
||||||
// Функция для определения яркости цвета
|
|
||||||
const getColorBrightness = (hexColor) => {
|
const getColorBrightness = (hexColor) => {
|
||||||
if (!hexColor) return 255;
|
if (!hexColor) return 255;
|
||||||
|
|
||||||
const color = hexColor.replace('#', '');
|
const color = hexColor.replace('#', '');
|
||||||
|
|
||||||
let r, g, b;
|
let r, g, b;
|
||||||
if (color.length === 3) {
|
if (color.length === 3) {
|
||||||
r = parseInt(color[0] + color[0], 16);
|
r = parseInt(color[0] + color[0], 16);
|
||||||
@ -48,174 +74,132 @@ const getColorBrightness = (hexColor) => {
|
|||||||
} else {
|
} else {
|
||||||
return 255;
|
return 255;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (0.299 * r + 0.587 * g + 0.114 * b);
|
return (0.299 * r + 0.587 * g + 0.114 * b);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Функция для подсветки текста
|
const formatNumber = (value, asInteger = false) => {
|
||||||
const highlightText = (text, searchQueries) => {
|
if (value === null || value === undefined || value === '') return String(value || '');
|
||||||
if (!text) return text;
|
const num = Number(value);
|
||||||
const cleanSearchQueries = searchQueries.filter(q => (q !== undefined && q !== ''));
|
if (isNaN(num)) return String(value);
|
||||||
if (cleanSearchQueries.length == 0) return text;
|
return num.toLocaleString('ru-RU', {
|
||||||
|
minimumFractionDigits: asInteger ? 0 : 1,
|
||||||
const escapedQueries = cleanSearchQueries.map(query => query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
maximumFractionDigits: asInteger ? 0 : 1,
|
||||||
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
useGrouping: true,
|
||||||
const parts = text.split(regex);
|
});
|
||||||
|
|
||||||
return parts.map((part, index) =>
|
|
||||||
regex.test(part) ? (
|
|
||||||
<mark
|
|
||||||
key={index}
|
|
||||||
style={{
|
|
||||||
backgroundColor: '#ffeb3b',
|
|
||||||
color: '#000',
|
|
||||||
fontWeight: 'bold',
|
|
||||||
padding: '0 2px',
|
|
||||||
borderRadius: '2px',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{part}
|
|
||||||
</mark>
|
|
||||||
) : (
|
|
||||||
part
|
|
||||||
),
|
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
const highlightWithFormatting = (originalValue, formattedValue, searchQueries) => {
|
// Оптимизированная функция подсветки
|
||||||
if (!originalValue && originalValue !== 0) return formattedValue;
|
const createHighlightedContent = (originalValue, displayValue, searchQueries) => {
|
||||||
|
const cleanQueries = searchQueries.filter(Boolean);
|
||||||
|
if (cleanQueries.length === 0) return displayValue;
|
||||||
|
|
||||||
const cleanSearchQueries = searchQueries.filter(q => (q !== undefined && q !== ''));
|
const searchStr = String(originalValue ?? displayValue);
|
||||||
if (cleanSearchQueries.length == 0) return formattedValue;
|
const escapedQueries = cleanQueries.map(q => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
||||||
|
|
||||||
const originalString = String(originalValue);
|
|
||||||
const escapedQueries = cleanSearchQueries.map(query => query.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
|
|
||||||
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');
|
||||||
|
|
||||||
if (!regex.test(originalString)) {
|
if (!regex.test(searchStr)) return displayValue;
|
||||||
return formattedValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
const parts = searchStr.split(regex);
|
||||||
<mark
|
return parts.map((part, index) => {
|
||||||
style={{
|
if (!regex.test(part)) return part;
|
||||||
|
return React.createElement('mark', {
|
||||||
|
key: index,
|
||||||
|
style: {
|
||||||
backgroundColor: '#ffeb3b',
|
backgroundColor: '#ffeb3b',
|
||||||
color: '#000',
|
color: '#000',
|
||||||
fontWeight: 'bold',
|
fontWeight: 'bold',
|
||||||
padding: '0 2px',
|
padding: '0 2px',
|
||||||
borderRadius: '2px',
|
borderRadius: '2px',
|
||||||
}}
|
},
|
||||||
>
|
}, part);
|
||||||
{formattedValue}
|
|
||||||
</mark>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
const INTEGER_COLUMN_IDS = new Set([
|
|
||||||
'data.header.num_group',
|
|
||||||
'data.nomenclature_group_id',
|
|
||||||
'data.header.item_id',
|
|
||||||
'data.article_id',
|
|
||||||
'data.code_razdela',
|
|
||||||
]);
|
|
||||||
|
|
||||||
// Функция для форматирования числа в финансовый формат
|
|
||||||
const formatNumber = (value, asInteger = false) => {
|
|
||||||
if (value === null || value === undefined || value === '') return String(value || '');
|
|
||||||
|
|
||||||
const num = Number(value);
|
|
||||||
if (isNaN(num)) return String(value);
|
|
||||||
|
|
||||||
if (asInteger) {
|
|
||||||
return num.toLocaleString('ru-RU', {
|
|
||||||
minimumFractionDigits: 0,
|
|
||||||
maximumFractionDigits: 0,
|
|
||||||
useGrouping: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
return num.toLocaleString('ru-RU', {
|
|
||||||
minimumFractionDigits: 1,
|
|
||||||
maximumFractionDigits: 1,
|
|
||||||
useGrouping: true,
|
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, columnFilter, backgroundColor, color, isEditable }) => {
|
const CellComponent = ({
|
||||||
|
cell,
|
||||||
|
row,
|
||||||
|
column,
|
||||||
|
onClick,
|
||||||
|
globalFilter,
|
||||||
|
columnFilter,
|
||||||
|
backgroundColor,
|
||||||
|
color,
|
||||||
|
isEditable,
|
||||||
|
isUpdating,
|
||||||
|
onCellNumberClick,
|
||||||
|
}) => {
|
||||||
const { lockedCells } = useRealtime();
|
const { lockedCells } = useRealtime();
|
||||||
const cellKey = `${row.id}_${column.id}`;
|
const cellKey = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
const isLocked = lockedCells.includes(cellKey);
|
const isLocked = lockedCells.includes(cellKey);
|
||||||
|
|
||||||
const cellValue = cell.getValue();
|
// Мемоизация значения
|
||||||
let originalValue = cellValue;
|
const { rawValue, displayValue, isNumeric } = useMemo(() => {
|
||||||
let displayValue = String(cellValue || '');
|
const value = cell.getValue();
|
||||||
|
const isNum = !isNaN(Number(value)) && value !== null && value !== undefined && value !== '';
|
||||||
|
let display = String(value || '');
|
||||||
|
|
||||||
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
if (isNum) {
|
||||||
if (isNumeric) {
|
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
||||||
const asInteger = INTEGER_COLUMN_IDS.has(column.id);
|
display = formatNumber(value, asInteger);
|
||||||
displayValue = formatNumber(cellValue, asInteger);
|
}
|
||||||
}
|
|
||||||
|
return { rawValue: value, displayValue: display, isNumeric: isNum };
|
||||||
|
}, [cell, column.id]);
|
||||||
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
if (!isNumeric) {
|
const searchQueries = [globalFilter, columnFilter];
|
||||||
return highlightText(displayValue, [globalFilter, columnFilter]);
|
return createHighlightedContent(rawValue, displayValue, searchQueries);
|
||||||
}
|
}, [rawValue, displayValue, globalFilter, columnFilter]);
|
||||||
return highlightWithFormatting(originalValue, displayValue, [globalFilter, columnFilter]);
|
|
||||||
}, [originalValue, displayValue, globalFilter, columnFilter, isNumeric]);
|
|
||||||
|
|
||||||
// Определяем цвет текста на основе яркости фона
|
const textColor = useMemo(() => {
|
||||||
const getTextColor = () => {
|
if (isLocked) return '#999999';
|
||||||
const brightness = getColorBrightness(backgroundColor);
|
const brightness = getColorBrightness(backgroundColor);
|
||||||
|
|
||||||
// Если яркость меньше 128 (темный фон) - используем белый текст, иначе черный
|
|
||||||
return brightness < 128 ? '#ffffff' : '#000000';
|
return brightness < 128 ? '#ffffff' : '#000000';
|
||||||
};
|
}, [backgroundColor, isLocked]);
|
||||||
|
|
||||||
// Базовые стили для заблокированной ячейки
|
const cellStyles = useMemo(() => ({
|
||||||
const lockedStyles = isLocked ? {
|
...BASE_CELL_STYLES,
|
||||||
border: '2px solid #e0e0e0',
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
backgroundColor: '#f5f5f5',
|
color: textColor,
|
||||||
cursor: 'not-allowed',
|
...(isLocked ? LOCKED_STYLES : {}),
|
||||||
opacity: 0.85,
|
}), [backgroundColor, isLocked, textColor]);
|
||||||
} : {};
|
|
||||||
|
|
||||||
|
const lockBadge = useMemo(() => {
|
||||||
|
if (!isLocked) return null;
|
||||||
|
return <div style={LOCK_BADGE_STYLES}>🔒</div>;
|
||||||
|
}, [isLocked]);
|
||||||
|
|
||||||
|
const handleClick = useCallback(() => {
|
||||||
// Определяем финальный цвет текста
|
if (!isLocked && onClick) {
|
||||||
const finalTextColor = isLocked ? '#999999' : getTextColor();
|
onClick();
|
||||||
|
}
|
||||||
|
}, [isLocked, onClick]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="cell"
|
className="cell"
|
||||||
style={{
|
style={cellStyles}
|
||||||
width: '100%',
|
onClick={handleClick}
|
||||||
height: '100%',
|
|
||||||
display: 'flex',
|
|
||||||
alignItems: 'center',
|
|
||||||
justifyContent: 'center',
|
|
||||||
boxSizing: 'border-box',
|
|
||||||
position: 'absolute',
|
|
||||||
top: 0,
|
|
||||||
left: 0,
|
|
||||||
right: 0,
|
|
||||||
bottom: 0,
|
|
||||||
border: '2px solid transparent',
|
|
||||||
padding: '8px',
|
|
||||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
|
||||||
color: finalTextColor,
|
|
||||||
...lockedStyles,
|
|
||||||
}}
|
|
||||||
onClick={isLocked ? undefined : onClick}
|
|
||||||
>
|
>
|
||||||
<ContainerForText>{highlightedContent}</ContainerForText>
|
<span style={CONTAINER_STYLES}>{highlightedContent}</span>
|
||||||
{isLocked && (
|
{lockBadge}
|
||||||
<LockBadge>
|
|
||||||
🔒
|
|
||||||
</LockBadge>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const Cell = React.memo(CellComponent, (prevProps, nextProps) => {
|
||||||
|
// Возвращаем true если пропсы равны (не нужно перерендеривать)
|
||||||
|
return (
|
||||||
|
prevProps.cell.getValue() === nextProps.cell.getValue() &&
|
||||||
|
prevProps.row.id === nextProps.row.id &&
|
||||||
|
prevProps.column.id === nextProps.column.id &&
|
||||||
|
prevProps.globalFilter === nextProps.globalFilter &&
|
||||||
|
prevProps.columnFilter === nextProps.columnFilter &&
|
||||||
|
prevProps.backgroundColor === nextProps.backgroundColor &&
|
||||||
|
prevProps.isEditable === nextProps.isEditable &&
|
||||||
|
prevProps.isUpdating === nextProps.isUpdating &&
|
||||||
|
prevProps.onCellNumberClick === nextProps.onCellNumberClick
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
export default Cell;
|
export default Cell;
|
||||||
@ -1,195 +1,228 @@
|
|||||||
// EditCell.js - обновленная версия
|
import React, { memo, useEffect, useRef, useLayoutEffect, useState, useCallback, useMemo } from 'react';
|
||||||
import { useEffect, useRef, useLayoutEffect, useState } from 'react';
|
|
||||||
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
import { isFormula, extractFormula, calculateFormula, formatNumber } from '../../utils/formulaUtils';
|
||||||
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
import { saveToStorage, loadFromStorage } from '../../utils/localStorageUtils';
|
||||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||||
|
|
||||||
const EditCell = ({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
const EDITOR_STYLES = {
|
||||||
|
position: 'absolute',
|
||||||
|
zIndex: 12,
|
||||||
|
top: 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
const FORMULA_INDICATOR_STYLES = {
|
||||||
|
position: 'absolute',
|
||||||
|
top: '-20px',
|
||||||
|
left: '4px',
|
||||||
|
fontSize: '10px',
|
||||||
|
color: '#666',
|
||||||
|
background: '#f0f0f0',
|
||||||
|
padding: '2px 6px',
|
||||||
|
borderRadius: '3px',
|
||||||
|
pointerEvents: 'none',
|
||||||
|
};
|
||||||
|
|
||||||
|
const TEXTAREA_STYLES = {
|
||||||
|
width: '100%',
|
||||||
|
height: 'auto',
|
||||||
|
fieldSizing: 'content',
|
||||||
|
fontSize: 'inherit',
|
||||||
|
};
|
||||||
|
|
||||||
|
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table }) => {
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
|
|
||||||
const [curValue, setCurValue] = useState(value);
|
|
||||||
const [displayValue, setDisplayValue] = useState(value);
|
|
||||||
const [isFormulaMode, setIsFormulaMode] = useState(false);
|
|
||||||
|
|
||||||
// Ключ для хранения формул в localStorage
|
|
||||||
const storageKey = `formula_${tableId}_${cellId}`;
|
const storageKey = `formula_${tableId}_${cellId}`;
|
||||||
|
|
||||||
const trFrag = refCell.current.offsetParent.offsetParent;
|
// Объединенное состояние
|
||||||
const tdFrag = refCell.current.offsetParent;
|
const [state, setState] = useState(() => ({
|
||||||
const left = tdFrag.offsetLeft;
|
currentValue: value,
|
||||||
const width = refCell.current.offsetParent.offsetWidth;
|
displayValue: value,
|
||||||
const translateY = trFrag.style.transform;
|
isFormulaMode: false,
|
||||||
|
}));
|
||||||
|
|
||||||
// Загружаем сохраненную формулу при монтировании
|
// Позиция ячейки
|
||||||
|
const [position, setPosition] = useState({ left: 0, width: 0, translateY: '' });
|
||||||
|
const textareaRef = useRef(null);
|
||||||
|
|
||||||
|
const refFinished = useRef(false);
|
||||||
|
|
||||||
|
// Загрузка сохраненной формулы
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const savedFormula = loadFromStorage(storageKey);
|
const savedFormula = loadFromStorage(storageKey);
|
||||||
if (savedFormula && isFormula(savedFormula)) {
|
if (savedFormula && isFormula(savedFormula)) {
|
||||||
setIsFormulaMode(true);
|
|
||||||
setCurValue(savedFormula);
|
|
||||||
// Вычисляем и отображаем результат
|
|
||||||
try {
|
try {
|
||||||
const formula = extractFormula(savedFormula);
|
const formula = extractFormula(savedFormula);
|
||||||
const result = calculateFormula(formula);
|
const result = calculateFormula(formula);
|
||||||
setDisplayValue(formatNumber(result));
|
setState({
|
||||||
} catch (error) {
|
currentValue: savedFormula,
|
||||||
setDisplayValue('#ОШИБКА');
|
displayValue: formatNumber(result),
|
||||||
|
isFormulaMode: true,
|
||||||
|
});
|
||||||
|
} catch {
|
||||||
|
setState({
|
||||||
|
currentValue: savedFormula,
|
||||||
|
displayValue: '#ОШИБКА',
|
||||||
|
isFormulaMode: true,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
setDisplayValue(value);
|
|
||||||
}
|
}
|
||||||
}, [storageKey, value]);
|
}, [storageKey]);
|
||||||
|
|
||||||
function useAutoResize(value) {
|
// Вычисление позиции
|
||||||
const ref = useRef(null);
|
useLayoutEffect(() => {
|
||||||
|
if (!refCell?.current) return;
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
const td = refCell.current.offsetParent;
|
||||||
const el = ref.current;
|
const tr = td?.offsetParent;
|
||||||
if (!el) return;
|
|
||||||
|
|
||||||
|
if (td && tr) {
|
||||||
|
setPosition({
|
||||||
|
left: td.offsetLeft,
|
||||||
|
width: td.offsetWidth,
|
||||||
|
translateY: tr.style.transform || '',
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, [refCell]);
|
||||||
|
|
||||||
|
// Auto-resize
|
||||||
|
useLayoutEffect(() => {
|
||||||
|
const el = textareaRef.current;
|
||||||
|
if (!el) return;
|
||||||
|
|
||||||
|
const resize = () => {
|
||||||
el.style.height = 'auto';
|
el.style.height = 'auto';
|
||||||
el.style.minHeight = '3rem';
|
el.style.minHeight = '3rem';
|
||||||
el.style.height = el.scrollHeight + 'px';
|
el.style.height = el.scrollHeight + 'px';
|
||||||
el.style.maxHeight = '180px';
|
el.style.maxHeight = '180px';
|
||||||
}, [value]);
|
};
|
||||||
|
|
||||||
return ref;
|
resize();
|
||||||
}
|
const observer = new ResizeObserver(resize);
|
||||||
|
observer.observe(el);
|
||||||
|
|
||||||
const ref = useAutoResize(value);
|
return () => observer.disconnect();
|
||||||
|
}, [state.currentValue]);
|
||||||
|
|
||||||
|
// Фокус при монтировании
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current && !disabled) {
|
const el = textareaRef.current;
|
||||||
ref.current.focus();
|
if (el && !disabled) {
|
||||||
ref.current.setSelectionRange(
|
el.focus();
|
||||||
ref.current.value.length,
|
el.setSelectionRange(el.value.length, el.value.length);
|
||||||
ref.current.value.length,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}, [disabled, ref]);
|
}, [disabled]);
|
||||||
|
|
||||||
const handleSave = () => {
|
// Сохранение
|
||||||
// Проверяем, является ли введенное значение формулой
|
const handleSave = useCallback(() => {
|
||||||
if (isFormula(curValue)) {
|
const { currentValue } = state;
|
||||||
// Сохраняем формулу в localStorage
|
|
||||||
saveToStorage(storageKey, curValue);
|
if (isFormula(currentValue)) {
|
||||||
// Вычисляем и сохраняем результат
|
saveToStorage(storageKey, currentValue);
|
||||||
try {
|
try {
|
||||||
const formula = extractFormula(curValue);
|
const formula = extractFormula(currentValue);
|
||||||
const result = calculateFormula(formula);
|
const result = calculateFormula(formula);
|
||||||
const formattedResult = formatNumber(result);
|
const formattedResult = formatNumber(result);
|
||||||
setDisplayValue(formattedResult);
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
displayValue: formattedResult,
|
||||||
|
isFormulaMode: true,
|
||||||
|
}));
|
||||||
onChange?.(formattedResult);
|
onChange?.(formattedResult);
|
||||||
setIsFormulaMode(true);
|
} catch {
|
||||||
return;
|
setState(prev => ({
|
||||||
} catch (error) {
|
...prev,
|
||||||
setDisplayValue('#ОШИБКА');
|
displayValue: '#ОШИБКА',
|
||||||
onChange?.(curValue); // Сохраняем как текст, если ошибка
|
isFormulaMode: true,
|
||||||
return;
|
}));
|
||||||
|
onChange?.(currentValue);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// Если это не формула, удаляем сохраненную формулу
|
|
||||||
localStorage.removeItem(storageKey);
|
localStorage.removeItem(storageKey);
|
||||||
setIsFormulaMode(false);
|
setState(prev => ({
|
||||||
setDisplayValue(curValue);
|
...prev,
|
||||||
onChange?.(curValue);
|
displayValue: currentValue,
|
||||||
|
isFormulaMode: false,
|
||||||
|
}));
|
||||||
|
onChange?.(currentValue);
|
||||||
}
|
}
|
||||||
};
|
}, [state, storageKey, onChange]);
|
||||||
|
|
||||||
const handleChangeValue = (event) => {
|
// Завершение редактирования
|
||||||
const input = event.target.value;
|
const finishEditing = useCallback(() => {
|
||||||
setCurValue(input);
|
if(refFinished.current) return;
|
||||||
// При редактировании показываем введенный текст
|
refFinished.current = true;
|
||||||
setDisplayValue(input);
|
|
||||||
};
|
|
||||||
|
|
||||||
// Функция для отображения значения в ячейке
|
|
||||||
const getDisplayValue = () => {
|
|
||||||
if (isFormulaMode && isFormula(curValue)) {
|
|
||||||
return displayValue;
|
|
||||||
}
|
|
||||||
return displayValue;
|
|
||||||
};
|
|
||||||
|
|
||||||
const finishEditing = () => {
|
|
||||||
if (!disabled) {
|
if (!disabled) {
|
||||||
handleSave();
|
handleSave();
|
||||||
}
|
}
|
||||||
const editingCell = table.getState().editingCell || null;
|
const editingCell = table.getState().editingCell;
|
||||||
if (editingCell) {
|
if (editingCell) {
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
}
|
}
|
||||||
};
|
}, [disabled, handleSave, table, contextEndEditing]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const handleClickOutside = (event) => {
|
const handleClickOutside = (event) => {
|
||||||
// Проверяем, был ли клик внутри элемента редактирования
|
const el = textareaRef.current;
|
||||||
const editElement = ref.current;
|
if (el && !el.contains(event.target)) {
|
||||||
if (editElement && !editElement.contains(event.target)) {
|
|
||||||
finishEditing();
|
finishEditing();
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
document.addEventListener('mousedown', handleClickOutside);
|
document.addEventListener('mousedown', handleClickOutside);
|
||||||
|
return () => document.removeEventListener('mousedown', handleClickOutside);
|
||||||
|
}, [finishEditing]);
|
||||||
|
|
||||||
return () => {
|
// Обработчики событий
|
||||||
document.removeEventListener('mousedown', handleClickOutside);
|
const handleChange = useCallback((e) => {
|
||||||
};
|
const value = e.target.value;
|
||||||
}, [ref, finishEditing]);
|
setState(prev => ({
|
||||||
|
...prev,
|
||||||
|
currentValue: value,
|
||||||
|
displayValue: value,
|
||||||
|
}));
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
const handleKeyDown = useCallback((e) => {
|
||||||
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
|
e.preventDefault();
|
||||||
|
finishEditing();
|
||||||
|
}
|
||||||
|
}, [finishEditing]);
|
||||||
|
|
||||||
|
// Мемоизация стилей
|
||||||
|
const editorStyles = useMemo(() => ({
|
||||||
|
...EDITOR_STYLES,
|
||||||
|
left: position.left,
|
||||||
|
width: position.width,
|
||||||
|
transform: position.translateY,
|
||||||
|
}), [position]);
|
||||||
|
|
||||||
|
const textareaStyles = useMemo(() => ({
|
||||||
|
...TEXTAREA_STYLES,
|
||||||
|
backgroundColor: state.isFormulaMode ? '#f8f9fa' : 'white',
|
||||||
|
border: state.isFormulaMode ? '2px solid #4a90d9' : undefined,
|
||||||
|
}), [state.isFormulaMode]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr>
|
<tr>
|
||||||
<td>
|
<td>
|
||||||
<div
|
<div style={editorStyles}>
|
||||||
key="blockForEdit"
|
{state.isFormulaMode && (
|
||||||
style={{
|
<div style={FORMULA_INDICATOR_STYLES}>fx</div>
|
||||||
position: 'absolute',
|
|
||||||
left: left,
|
|
||||||
zIndex: 12,
|
|
||||||
width: width,
|
|
||||||
transform: translateY,
|
|
||||||
top: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{isFormulaMode && (
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
top: '-20px',
|
|
||||||
left: '4px',
|
|
||||||
fontSize: '10px',
|
|
||||||
color: '#666',
|
|
||||||
background: '#f0f0f0',
|
|
||||||
padding: '2px 6px',
|
|
||||||
borderRadius: '3px',
|
|
||||||
pointerEvents: 'none'
|
|
||||||
}}>
|
|
||||||
fx
|
|
||||||
</div>
|
|
||||||
)}
|
)}
|
||||||
<textarea
|
<textarea
|
||||||
ref={ref}
|
ref={textareaRef}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
style={{
|
style={textareaStyles}
|
||||||
width: '100%',
|
onChange={handleChange}
|
||||||
height: 'auto',
|
value={state.currentValue}
|
||||||
fieldSizing: 'content',
|
onKeyDown={handleKeyDown}
|
||||||
fontSize: 'inherit',
|
placeholder={state.isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
||||||
backgroundColor: isFormulaMode ? '#f8f9fa' : 'white',
|
|
||||||
border: isFormulaMode ? '2px solid #4a90d9' : undefined,
|
|
||||||
}}
|
|
||||||
onChange={handleChangeValue}
|
|
||||||
value={curValue}
|
|
||||||
onKeyDown={(e) => {
|
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
|
||||||
e.preventDefault();
|
|
||||||
finishEditing();
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
placeholder={isFormulaMode ? 'Введите формулу (начинается с =)' : ''}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
);
|
);
|
||||||
};
|
});
|
||||||
|
|
||||||
|
EditCell.displayName = 'EditCell';
|
||||||
|
|
||||||
export default EditCell;
|
export default EditCell;
|
||||||
@ -28,6 +28,7 @@ import { CircularProgress } from '@mui/material';
|
|||||||
import { additionVspRowTable } from './constants/addingRowConfig';
|
import { additionVspRowTable } from './constants/addingRowConfig';
|
||||||
import { SelectVspModal } from './Modals/SelectVspModal';
|
import { SelectVspModal } from './Modals/SelectVspModal';
|
||||||
import { getRowId } from './utils/rowUtils';
|
import { getRowId } from './utils/rowUtils';
|
||||||
|
import { debounce } from '@mui/material';
|
||||||
|
|
||||||
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
||||||
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction, formType, year);
|
||||||
@ -47,11 +48,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
return additionVspRowTable[formType].includes(sheetName);
|
return additionVspRowTable[formType].includes(sheetName);
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
const handleGlobalFilterChange = (value) => {
|
const handleGlobalFilterChange = useCallback(
|
||||||
startTransition(() => {
|
debounce((value) => {
|
||||||
setGlobalFilter(value);
|
startTransition(() => {
|
||||||
});
|
setGlobalFilter(value);
|
||||||
};
|
});
|
||||||
|
}, 300),
|
||||||
|
[]
|
||||||
|
);
|
||||||
|
|
||||||
const {
|
const {
|
||||||
isConnected,
|
isConnected,
|
||||||
@ -89,17 +93,27 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
|
|
||||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||||
|
|
||||||
|
const configCache = useRef(new Map());
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (!formType || !sheetName) return;
|
||||||
|
const cacheKey = `${formType}_${sheetName}`;
|
||||||
|
|
||||||
|
if (configCache.current.has(cacheKey)) {
|
||||||
|
setColumnsConfig(configCache.current.get(cacheKey));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const loadConfig = async () => {
|
const loadConfig = async () => {
|
||||||
try {
|
try {
|
||||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||||
|
configCache.current.set(cacheKey, config.config);
|
||||||
setColumnsConfig(config.config);
|
setColumnsConfig(config.config);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||||
setColumnsConfig({ columns: [], colors: {} });
|
setColumnsConfig({ columns: [], colors: {} });
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
loadConfig();
|
loadConfig();
|
||||||
}, [formType, sheetName]);
|
}, [formType, sheetName]);
|
||||||
|
|
||||||
@ -154,7 +168,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
console.error('Error creating columns:', error);
|
console.error('Error creating columns:', error);
|
||||||
return [];
|
return [];
|
||||||
}
|
}
|
||||||
}, [columnsConfig, handleUpdateCell, handleClickRowCell, handleCellUpdateError]);
|
}, [columnsConfig]);
|
||||||
|
|
||||||
const selectedColumnIdRef = useRef(selectedColumnId);
|
const selectedColumnIdRef = useRef(selectedColumnId);
|
||||||
|
|
||||||
@ -168,7 +182,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
}), [handleUpdateCell]);
|
}), [handleUpdateCell]);
|
||||||
|
|
||||||
const ROW_VIRTUALIZER_OPTIONS = {
|
const ROW_VIRTUALIZER_OPTIONS = {
|
||||||
overscan: 30,
|
overscan: 5,
|
||||||
scrollPaddingStart: 0,
|
scrollPaddingStart: 0,
|
||||||
scrollPaddingEnd: 0,
|
scrollPaddingEnd: 0,
|
||||||
estimateSize: () => TABLE_ROW_HEIGHT,
|
estimateSize: () => TABLE_ROW_HEIGHT,
|
||||||
@ -306,43 +320,34 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells || !dataEditingCells.line_id) {
|
if (!dataEditingCells?.line_id) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
if (editingCell) return;
|
if (editingCell) return;
|
||||||
const rowId = dataEditingCells.line_id;
|
|
||||||
const columnId = dataEditingCells.column;
|
// Отложить поиск до следующего тика
|
||||||
try {
|
requestAnimationFrame(() => {
|
||||||
const row = table.getRow(rowId);
|
try {
|
||||||
const column = table.getColumn(columnId);
|
const row = table.getRow(dataEditingCells.line_id);
|
||||||
const cell = row?.getVisibleCells().find(c => c.column.id === columnId);
|
const cell = row?.getVisibleCells().find(
|
||||||
if (cell) {
|
c => c.column.id === dataEditingCells.column
|
||||||
setEditingCell(cell);
|
);
|
||||||
|
if (cell) setEditingCell(cell);
|
||||||
|
} catch (error) {
|
||||||
|
console.error(error);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
});
|
||||||
console.error(error);
|
}, [dataEditingCells, editingCell, table]);
|
||||||
toast.error('Ошибка редактирования ячейки');
|
|
||||||
}
|
const [isFirstLoad, setIsFirstLoad] = useState(true);
|
||||||
}, [dataEditingCells, editingCell]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (table && data && columns.length !== 0) {
|
if (isFirstLoad && table && data && columns.length !== 0) {
|
||||||
setIsLoadingData(true)
|
setIsFirstLoad(false);
|
||||||
|
setIsLoadingData(true);
|
||||||
}
|
}
|
||||||
}, [data, columns, table])
|
}, [data, columns, table, isFirstLoad]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
// Принудительно обновляем состояние закрепленных колонок
|
|
||||||
// если так не сделать при скролле будут пропадать закрепленные колонки
|
|
||||||
if (columnPinning && Object.keys(columnPinning).length > 0) {
|
|
||||||
const timer = setTimeout(() => {
|
|
||||||
setColumnPinning(prev => ({ ...prev }));
|
|
||||||
}, 100);
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
|
||||||
}
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const handleAddRow = useCallback(() => {
|
const handleAddRow = useCallback(() => {
|
||||||
const allRows = table.getRowModel().rows;
|
const allRows = table.getRowModel().rows;
|
||||||
|
|||||||
@ -1,11 +1,11 @@
|
|||||||
import React, { useEffect, useRef, useState } from 'react';
|
// getTableColumns.js
|
||||||
|
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||||||
import { createPortal } from 'react-dom';
|
import { createPortal } from 'react-dom';
|
||||||
import { columns as mockColumns } from '../../mocks/mockTable';
|
|
||||||
import { config } from './constants/FORM_2/AHR';
|
|
||||||
import { useParams } from 'react-router';
|
import { useParams } from 'react-router';
|
||||||
import { useRealtime } from './contexts/RealtimeContext';
|
import { useRealtime } from './contexts/RealtimeContext';
|
||||||
|
|
||||||
function EditCellPortal({
|
// Мемоизированный компонент EditCellPortal
|
||||||
|
const EditCellPortal = React.memo(({
|
||||||
cell,
|
cell,
|
||||||
table,
|
table,
|
||||||
EditCell,
|
EditCell,
|
||||||
@ -13,89 +13,78 @@ function EditCellPortal({
|
|||||||
onSaveEnd,
|
onSaveEnd,
|
||||||
onError,
|
onError,
|
||||||
isEditable
|
isEditable
|
||||||
}) {
|
}) => {
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||||||
const ref = useRef(null);
|
const ref = useRef(null);
|
||||||
const [refTbody, setRefTbody] = useState(false);
|
const [refTbody, setRefTbody] = useState(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const isDisabled = false;
|
|
||||||
|
|
||||||
const { endEditing: contextEndEditing } = useRealtime();
|
const { endEditing: contextEndEditing } = useRealtime();
|
||||||
|
|
||||||
|
// Оптимизация: вычисляем позицию только при монтировании и изменении cell
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ref.current) {
|
if (ref.current) {
|
||||||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||||||
setRefTbody(tbodyRef);
|
if (tbodyRef && tbodyRef !== refTbody) {
|
||||||
|
setRefTbody(tbodyRef);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, []);
|
}, [refTbody]); // Добавляем refTbody в зависимости
|
||||||
|
|
||||||
const handleChange = async (val) => {
|
// Мемоизация обработчика изменения
|
||||||
|
const handleChange = useCallback( (val) => {
|
||||||
table.setEditingCell(null);
|
table.setEditingCell(null);
|
||||||
try {
|
try {
|
||||||
if (table.options.meta?.updateCell) {
|
if (table.options.meta?.updateCell) {
|
||||||
const success = await table.options.meta.updateCell(
|
const success = table.options.meta.updateCell(
|
||||||
cell.row,
|
cell.row,
|
||||||
cell.column,
|
cell.column,
|
||||||
val
|
val
|
||||||
);
|
);
|
||||||
if (success) {
|
if (!success) {
|
||||||
console.log('Cell updated successfully via WebSocket');
|
|
||||||
} else {
|
|
||||||
console.error('Failed to update cell via WebSocket');
|
console.error('Failed to update cell via WebSocket');
|
||||||
const errorMsg = 'Не удалось сохранить изменение';
|
onError?.('Не удалось сохранить изменение');
|
||||||
onError?.(errorMsg);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Error updating cell:', error);
|
console.error('Error updating cell:', error);
|
||||||
onError?.(error.message);
|
onError?.(error.message);
|
||||||
}
|
}
|
||||||
};
|
}, [table, cell, onError]);
|
||||||
|
|
||||||
|
// Мемоизация пропсов для EditCell
|
||||||
|
const editCellProps = useMemo(() => ({
|
||||||
|
refCell: ref,
|
||||||
|
cell,
|
||||||
|
value: cell.getValue(),
|
||||||
|
disabled: !isEditable,
|
||||||
|
onChange: handleChange,
|
||||||
|
tableId: tableKey,
|
||||||
|
table,
|
||||||
|
cellId: `${cell.row.id}_${cell.column.id}`,
|
||||||
|
}), [cell, isEditable, handleChange, tableKey, table]);
|
||||||
|
|
||||||
|
// Мемоизация портала
|
||||||
|
const portalContent = useMemo(() => {
|
||||||
|
if (!refTbody || isSaving) return null;
|
||||||
|
return createPortal(
|
||||||
|
<EditCell {...editCellProps} />,
|
||||||
|
refTbody
|
||||||
|
);
|
||||||
|
}, [refTbody, isSaving, editCellProps, EditCell]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div
|
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
||||||
ref={ref}
|
{portalContent}
|
||||||
style={{
|
|
||||||
width: '100%',
|
|
||||||
height: '100%',
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
{refTbody && !isSaving &&
|
|
||||||
createPortal(
|
|
||||||
<EditCell
|
|
||||||
refCell={ref}
|
|
||||||
cell={cell}
|
|
||||||
value={cell.getValue()}
|
|
||||||
disabled={!isEditable}
|
|
||||||
onChange={handleChange}
|
|
||||||
tableId={tableKey}
|
|
||||||
table={table}
|
|
||||||
cellId={cell.row.id + '_' + cell.column.id}
|
|
||||||
/>,
|
|
||||||
refTbody,
|
|
||||||
)}
|
|
||||||
{isSaving && (
|
|
||||||
createPortal(
|
|
||||||
<div style={{
|
|
||||||
position: 'absolute',
|
|
||||||
background: 'rgba(0,0,0,0.5)',
|
|
||||||
color: 'white',
|
|
||||||
padding: '4px 8px',
|
|
||||||
borderRadius: '4px',
|
|
||||||
fontSize: '12px',
|
|
||||||
zIndex: 1000
|
|
||||||
}}>
|
|
||||||
Saving...
|
|
||||||
</div>,
|
|
||||||
refTbody
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
});
|
||||||
|
|
||||||
|
EditCellPortal.displayName = 'EditCellPortal';
|
||||||
|
|
||||||
|
// Основная функция getTableColumns
|
||||||
export const getTableColumns = ({
|
export const getTableColumns = ({
|
||||||
Cell,
|
Cell,
|
||||||
EditCell,
|
EditCell,
|
||||||
@ -104,54 +93,127 @@ export const getTableColumns = ({
|
|||||||
onCellNumberClick,
|
onCellNumberClick,
|
||||||
}) => {
|
}) => {
|
||||||
const columnColors = { ...columnsConfig.colors };
|
const columnColors = { ...columnsConfig.colors };
|
||||||
const columns = [...columnsConfig.columns];
|
const columns = structuredClone(columnsConfig.columns);
|
||||||
|
|
||||||
const processColumns = (columns, EditCell, Cell) => {
|
// Кэши для вычисленных значений
|
||||||
|
const cellPropsCache = new WeakMap();
|
||||||
|
const editPropsCache = new WeakMap();
|
||||||
|
|
||||||
|
// Функция для получения кешированных пропсов Cell
|
||||||
|
const getCachedCellProps = (row, column, table) => {
|
||||||
|
const key = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
|
if (!cellPropsCache.has(row)) {
|
||||||
|
cellPropsCache.set(row, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCache = cellPropsCache.get(row);
|
||||||
|
if (rowCache[key]) {
|
||||||
|
// Проверяем, не изменились ли фильтры
|
||||||
|
const cached = rowCache[key];
|
||||||
|
const currentGlobalFilter = table.getState().globalFilter;
|
||||||
|
const currentColumnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
||||||
|
|
||||||
|
if (cached.globalFilter === currentGlobalFilter &&
|
||||||
|
cached.columnFilter === currentColumnFilter) {
|
||||||
|
return cached;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const globalFilter = table.getState().globalFilter;
|
||||||
|
const columnFilter = table.getState().columnFilters?.find(f => f.id === column.id)?.value;
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
globalFilter,
|
||||||
|
columnFilter,
|
||||||
|
isEditable: row.original?.row_type === 'INPUT' || false,
|
||||||
|
backgroundColor: columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type],
|
||||||
|
isUpdating: table.options.meta?.updatingCells?.[`${row.id}_${column.id}`],
|
||||||
|
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}`,
|
||||||
|
};
|
||||||
|
|
||||||
|
rowCache[key] = props;
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Функция для получения кешированных пропсов Edit
|
||||||
|
const getCachedEditProps = (row, column, table) => {
|
||||||
|
const key = `${row.id}_${column.id}`;
|
||||||
|
|
||||||
|
if (!editPropsCache.has(row)) {
|
||||||
|
editPropsCache.set(row, {});
|
||||||
|
}
|
||||||
|
|
||||||
|
const rowCache = editPropsCache.get(row);
|
||||||
|
if (rowCache[key]) {
|
||||||
|
return rowCache[key];
|
||||||
|
}
|
||||||
|
|
||||||
|
const props = {
|
||||||
|
isEditable: row.original?.row_type === 'INPUT' || false,
|
||||||
|
};
|
||||||
|
|
||||||
|
rowCache[key] = props;
|
||||||
|
return props;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Оптимизированная функция processColumns
|
||||||
|
const processColumns = (columns) => {
|
||||||
columns.forEach((col) => {
|
columns.forEach((col) => {
|
||||||
col.Cell = ({ cell, table, column, row }) => (
|
// Оптимизированный Cell
|
||||||
<Cell
|
col.Cell = ({ cell, table, column, row }) => {
|
||||||
row={row}
|
const props = getCachedCellProps(row, column, table);
|
||||||
column={column}
|
|
||||||
cell={cell}
|
|
||||||
globalFilter={
|
|
||||||
table.getState().globalFilter
|
|
||||||
}
|
|
||||||
columnFilter={table.getState().columnFilters?.find(f => f.id === column.id)?.value}
|
|
||||||
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}`]}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
|
|
||||||
col.Edit = ({ cell, table, row, column }) => (
|
// Используем useMemo для мемоизации пропсов
|
||||||
<EditCellPortal
|
const cellProps = useMemo(() => ({
|
||||||
cell={cell}
|
row,
|
||||||
table={table}
|
column,
|
||||||
EditCell={EditCell}
|
cell,
|
||||||
onError={onCellUpdateError}
|
...props,
|
||||||
isEditable={row.original?.row_type === 'INPUT' || false}
|
}), [row.id, column.id, cell.getValue(), props._hash]);
|
||||||
/>
|
|
||||||
);
|
return <Cell {...cellProps} />;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Оптимизированный Edit
|
||||||
|
col.Edit = ({ cell, table, row, column }) => {
|
||||||
|
const props = getCachedEditProps(row, column, table);
|
||||||
|
|
||||||
|
// Мемоизируем компонент EditCellPortal
|
||||||
|
const editComponent = useMemo(() => (
|
||||||
|
<EditCellPortal
|
||||||
|
cell={cell}
|
||||||
|
table={table}
|
||||||
|
EditCell={EditCell}
|
||||||
|
onError={onCellUpdateError}
|
||||||
|
isEditable={props.isEditable}
|
||||||
|
/>
|
||||||
|
), [cell, table, EditCell, onCellUpdateError, props.isEditable]);
|
||||||
|
|
||||||
|
return editComponent;
|
||||||
|
};
|
||||||
|
|
||||||
// Рекурсивно обрабатываем вложенные колонки
|
// Рекурсивно обрабатываем вложенные колонки
|
||||||
if (col.columns?.length) {
|
if (col.columns?.length) {
|
||||||
processColumns(col.columns, EditCell, Cell);
|
processColumns(col.columns);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
processColumns(columns, EditCell, Cell);
|
processColumns(columns);
|
||||||
|
|
||||||
|
// Оптимизированная колонка с номерами строк
|
||||||
const rowNumber = {
|
const rowNumber = {
|
||||||
accessorKey: 'sort_order',
|
accessorKey: 'sort_order',
|
||||||
header: '#',
|
header: '#',
|
||||||
size: 50,
|
size: 50,
|
||||||
enableEditing: false,
|
enableEditing: false,
|
||||||
Cell: ({ cell, row }) => {
|
Cell: ({ cell, row }) => {
|
||||||
return <button onClick={() => { onCellNumberClick(row.id) }}>
|
const handleClick = useCallback(() => {
|
||||||
{cell.getValue() + ' '}
|
onCellNumberClick(row.id);
|
||||||
</button>
|
}, [row.id, onCellNumberClick]);
|
||||||
|
|
||||||
|
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user