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