210 lines
5.7 KiB
JavaScript
210 lines
5.7 KiB
JavaScript
import React, { useEffect, useRef, useState, useMemo, useCallback } from 'react';
|
||
import { createPortal } from 'react-dom';
|
||
import { useParams } from 'react-router';
|
||
import { useRealtime } from './contexts/RealtimeContext';
|
||
|
||
const EditCellPortal = React.memo(({
|
||
cell,
|
||
table,
|
||
EditCell,
|
||
onSaveStart,
|
||
onSaveEnd,
|
||
onError,
|
||
isEditable
|
||
}) => {
|
||
const { formId, formType, sheetName, direction } = useParams();
|
||
const tableKey = `${formId}_${formType}_${sheetName}_${direction}`;
|
||
const ref = useRef(null);
|
||
const [refTbody, setRefTbody] = useState(null);
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
|
||
const { endEditing: contextEndEditing } = useRealtime();
|
||
|
||
useEffect(() => {
|
||
if (ref.current) {
|
||
const tbodyRef = ref.current.offsetParent?.offsetParent?.offsetParent;
|
||
if (tbodyRef && tbodyRef !== refTbody) {
|
||
setRefTbody(tbodyRef);
|
||
}
|
||
}
|
||
}, []);
|
||
|
||
const handleChange = useCallback( (val) => {
|
||
table.setEditingCell(null);
|
||
try {
|
||
if (table.options.meta?.updateCell) {
|
||
const success = table.options.meta.updateCell(
|
||
cell.row,
|
||
cell.column,
|
||
val
|
||
);
|
||
if (!success) {
|
||
console.error('Failed to update cell via WebSocket');
|
||
onError?.('Не удалось сохранить изменение');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
console.error('Error updating cell:', error);
|
||
onError?.(error.message);
|
||
}
|
||
}, [table, cell, onError]);
|
||
|
||
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 (
|
||
<>
|
||
<div ref={ref} style={{ width: '100%', height: '100%' }} />
|
||
{portalContent}
|
||
</>
|
||
);
|
||
});
|
||
|
||
|
||
export const getTableColumns = ({
|
||
Cell,
|
||
EditCell,
|
||
columnsConfig,
|
||
onCellUpdateError,
|
||
onCellNumberClick,
|
||
isCellInvalid,
|
||
}) => {
|
||
const columnColors = { ...columnsConfig.colors };
|
||
const columns = structuredClone(columnsConfig.columns);
|
||
|
||
const cellPropsCache = new WeakMap();
|
||
const editPropsCache = new WeakMap();
|
||
|
||
const getCachedCellProps = (row, column, table) => {
|
||
const key = `${row.id}_${column.id}`;
|
||
const value = row.getValue(column.id);
|
||
const isInvalid = isCellInvalid?.(column.id, value, row.original) || false;
|
||
|
||
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 &&
|
||
cached.value === value &&
|
||
cached.isInvalid === isInvalid) {
|
||
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}`],
|
||
isInvalid,
|
||
value,
|
||
_hash: `${globalFilter}_${columnFilter}_${row.id}_${column.id}_${value}_${isInvalid}`,
|
||
};
|
||
|
||
rowCache[key] = props;
|
||
return props;
|
||
};
|
||
|
||
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) => {
|
||
col.Cell = ({ cell, table, column, row }) => {
|
||
const props = getCachedCellProps(row, column, table);
|
||
|
||
const cellProps = useMemo(() => ({
|
||
row,
|
||
column,
|
||
cell,
|
||
...props,
|
||
}), [row.id, column.id, cell.getValue(), props._hash]);
|
||
|
||
return <Cell {...cellProps} />;
|
||
};
|
||
|
||
col.Edit = ({ cell, table, row, column }) => {
|
||
const props = getCachedEditProps(row, column, table);
|
||
|
||
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) {
|
||
processColumns(col.columns);
|
||
}
|
||
});
|
||
};
|
||
|
||
processColumns(columns);
|
||
|
||
const rowNumber = {
|
||
accessorKey: 'sort_order',
|
||
header: '#',
|
||
size: 50,
|
||
enableEditing: false,
|
||
Cell: ({ cell, row }) => {
|
||
const handleClick = useCallback(() => {
|
||
onCellNumberClick(row.id);
|
||
}, [row.id, onCellNumberClick]);
|
||
|
||
return <button onClick={handleClick}>{cell.getValue() + ' '}</button>;
|
||
},
|
||
};
|
||
|
||
return [rowNumber, ...columns];
|
||
}; |