settingPanel fix: исправление ошибок с поискам + переделывания поиска с фильтра на скролл

This commit is contained in:
PotapovaA 2026-08-21 15:24:52 +03:00
parent d9d67a2410
commit ab2ad7fe06
3 changed files with 225 additions and 23 deletions

View File

@ -110,7 +110,7 @@ const createHighlightedContent = (originalValue, displayValue, searchQueries) =>
const cleanQueries = searchQueries.filter(Boolean); const cleanQueries = searchQueries.filter(Boolean);
if (cleanQueries.length === 0) return displayValue; if (cleanQueries.length === 0) return displayValue;
const searchStr = String(originalValue ?? displayValue); const searchStr = String(displayValue ?? originalValue);
const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); const escapedQueries = cleanQueries.map((q) => q.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'));
const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi'); const regex = new RegExp(`(${escapedQueries.join('|')})`, 'gi');

View File

@ -17,7 +17,7 @@ import { SelectExpenseItemModal } from './Modals/SelectExpenseItemModal';
import { SelectVspModal } from './Modals/SelectVspModal'; import { SelectVspModal } from './Modals/SelectVspModal';
import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig'; import { additionExpenseRowTable, additionVspRowTable } from './constants/addingRowConfig';
import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig'; import { BASE_TABLE_CONFIG, TABLE_ROW_HEIGHT, getTableBodyCellProps, getTablePaperStyles } from './constants/tableConfig';
import { useRealtimeActions } from './contexts/RealtimeContext'; import { useRealtimeActions, useVspOptions } from './contexts/RealtimeContext';
import { useColumnSettings } from './hooks/useColumnSettings'; import { useColumnSettings } from './hooks/useColumnSettings';
import { useHeaderPortal } from './hooks/useHeaderPortal'; import { useHeaderPortal } from './hooks/useHeaderPortal';
import { useTableScale } from './hooks/useTableScale'; import { useTableScale } from './hooks/useTableScale';
@ -39,15 +39,15 @@ const ROW_VIRTUALIZER_OPTIONS = {
}; };
const getColumnVirtualizerOptions = ({ table }) => { const getColumnVirtualizerOptions = ({ table }) => {
const orderedVisibleColumns = [ const orderedVisibleColumns = table.getVisibleLeafColumns();
...table.getLeftVisibleLeafColumns(),
...table.getCenterVisibleLeafColumns(),
...table.getRightVisibleLeafColumns(),
];
const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150; const getColumnSize = (index) => orderedVisibleColumns[index]?.getSize() ?? 150;
const scrollPaddingStart = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
const scrollPaddingEnd = table.getRightVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
return { return {
overscan: 10, overscan: 10,
scrollPaddingStart,
scrollPaddingEnd,
estimateSize: getColumnSize, estimateSize: getColumnSize,
measureElement: (element) => { measureElement: (element) => {
if (!element) return 150; if (!element) return 150;
@ -59,6 +59,18 @@ const getColumnVirtualizerOptions = ({ table }) => {
const hasVspDropdown = (columns) => const hasVspDropdown = (columns) =>
columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns))); columns.some((column) => column.editType === 'vsp_dropdown' || (column.columns?.length && hasVspDropdown(column.columns)));
const normalizeFilterValue = (value) =>
String(value ?? '')
.trim()
.toLocaleLowerCase('ru-RU');
const doesCellMatchFilter = (rawValue, columnDef, query, vspRegistrationById) => {
if (normalizeFilterValue(rawValue).includes(query)) return true;
if (columnDef?.editType !== 'vsp_dropdown') return false;
return vspRegistrationById.get(Number(rawValue))?.includes(query) ?? false;
};
const tableContentStyle = { const tableContentStyle = {
position: 'relative', position: 'relative',
width: '100%', width: '100%',
@ -79,6 +91,7 @@ const loadingOverlayStyle = {
const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => { const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const { user } = useAuth(); const { user } = useAuth();
const vspOptions = useVspOptions();
const userRoleId = user?.role_id; const userRoleId = user?.role_id;
const { const {
data, data,
@ -87,6 +100,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
editingCells: dataEditingCells, editingCells: dataEditingCells,
} = useRealtimeData(formId, sheetName, direction, formType, year); } = useRealtimeData(formId, sheetName, direction, formType, year);
const [globalFilter, setGlobalFilter] = useState(''); const [globalFilter, setGlobalFilter] = useState('');
const [searchMatchIndex, setSearchMatchIndex] = useState(-1);
const [activeSearchMatch, setActiveSearchMatch] = useState(null);
const [pendingSearchDirection, setPendingSearchDirection] = useState(0);
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({});
@ -120,9 +136,9 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const handleGlobalFilterChange = useMemo( const handleGlobalFilterChange = useMemo(
() => () =>
debounce((value) => { debounce((valueOrUpdater) => {
startTransition(() => { startTransition(() => {
setGlobalFilter(value); setGlobalFilter((currentValue) => (typeof valueOrUpdater === 'function' ? valueOrUpdater(currentValue) : valueOrUpdater));
}); });
}, 300), }, 300),
[startTransition], [startTransition],
@ -300,6 +316,23 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
[handleUpdateCell], [handleUpdateCell],
); );
const vspRegistrationById = useMemo(
() => new Map(vspOptions.map((vsp) => [Number(vsp.id), normalizeFilterValue(vsp.registration_number)])),
[vspOptions],
);
const globalFilterFn = useCallback(
(row, columnId, filterValue) => {
const query = normalizeFilterValue(filterValue);
if (!query) return true;
const rawValue = row.getValue(columnId);
const column = row.getAllCells().find((cell) => cell.column.id === columnId)?.column;
return doesCellMatchFilter(rawValue, column?.columnDef, query, vspRegistrationById);
},
[vspRegistrationById],
);
const createTableConfig = ({ const createTableConfig = ({
columns, columns,
data, data,
@ -317,8 +350,11 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setExpanded, setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
globalFilterFn,
activeSearchMatch,
}) => ({ }) => ({
...BASE_TABLE_CONFIG, ...BASE_TABLE_CONFIG,
globalFilterFn,
columns, columns,
data, data,
enableRowVirtualization: true, enableRowVirtualization: true,
@ -355,7 +391,21 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
muiTableHeadCellProps: { muiTableHeadCellProps: {
sx: { boxSizing: 'border-box' }, sx: { boxSizing: 'border-box' },
}, },
muiTableBodyCellProps: getTableBodyCellProps, muiTableBodyCellProps: (props) => {
const cellProps = getTableBodyCellProps(props);
const isActiveMatch = activeSearchMatch?.rowId === props.row.id && activeSearchMatch?.columnId === props.column.id;
return {
...cellProps,
sx: {
...cellProps.sx,
...(isActiveMatch && {
zIndex: 7,
boxShadow: 'inset 0 0 0 3px #1976d2',
}),
},
};
},
muiTableHeadProps: { muiTableHeadProps: {
sx: { sx: {
display: 'table-header-group', display: 'table-header-group',
@ -406,6 +456,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setExpanded, setExpanded,
editingCell, editingCell,
contextStartEditing, contextStartEditing,
globalFilterFn,
activeSearchMatch,
}), }),
[ [
columns, columns,
@ -422,6 +474,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
setColumnPinning, setColumnPinning,
editingCell, editingCell,
expanded, expanded,
globalFilterFn,
activeSearchMatch,
], ],
); );
@ -431,31 +485,108 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
const handleNavigateToColumn = useCallback( const handleNavigateToColumn = useCallback(
(columnId) => { (columnId) => {
if (!columnId) return; if (!columnId) return;
console.log(columnId);
console.log(columnId);
console.log(columnId);
console.log(columnId);
setSelectedColumnId(columnId); setSelectedColumnId(columnId);
const pinnedIds = new Set([...(columnPinning?.left || []), ...(columnPinning?.right || [])]);
if (pinnedIds.has(columnId)) return;
const visibleColumns = table.getVisibleLeafColumns();
const columnIndex = visibleColumns.findIndex((column) => column.id === columnId);
if (columnIndex < 0) return;
const columnVirtualizer = columnVirtualizerRef.current;
const container = containerRef.current; const container = containerRef.current;
if (!container) return; if (!container) return;
const pinnedIds = new Set(columnPinning?.left || []); const leftPinnedSize = table.getLeftVisibleLeafColumns().reduce((width, column) => width + column.getSize(), 0);
if (pinnedIds.has(columnId)) return; const calculatedStart = visibleColumns.slice(0, columnIndex).reduce((offset, column) => offset + column.getSize(), 0);
const measurement = columnVirtualizer?.measurementsCache?.[columnIndex];
const centerColumns = table.getVisibleLeafColumns().filter((column) => !pinnedIds.has(column.id)); const targetStart = measurement?.start ?? calculatedStart;
const targetOffset = Math.max(0, targetStart - leftPinnedSize);
let offset = 0;
for (const column of centerColumns) {
if (column.id === columnId) break;
offset += column.getSize();
}
container.scrollTo({ container.scrollTo({
left: Math.max(0, offset - 40), left: targetOffset,
behavior: 'smooth',
}); });
}, },
[columnPinning, containerRef, table], [columnPinning, containerRef, table],
); );
const searchMatches = useMemo(() => {
const query = normalizeFilterValue(globalFilter);
if (!query) return [];
const matches = [];
for (const [rowIndex, row] of table.getRowModel().rows.entries()) {
for (const cell of row.getVisibleCells()) {
if (doesCellMatchFilter(cell.getValue(), cell.column.columnDef, query, vspRegistrationById)) {
matches.push({ rowId: row.id, rowIndex, columnId: cell.column.id });
}
}
}
return matches;
}, [data, globalFilter, table, vspRegistrationById, columnVisibility]);
const navigateToSearchMatch = useCallback(
(direction) => {
if (!searchMatches.length) return;
setSearchMatchIndex((currentIndex) => {
const startIndex = currentIndex < 0 ? (direction > 0 ? -1 : 0) : currentIndex;
const nextIndex = (startIndex + direction + searchMatches.length) % searchMatches.length;
const match = searchMatches[nextIndex];
setActiveSearchMatch(match);
setRowSelection({ [match.rowId]: true });
console.log(match.columnId);
handleNavigateToColumn(match.columnId);
requestAnimationFrame(() => {
rowVirtualizerRef.current?.scrollToIndex?.(match.rowIndex < 5 ? match.rowIndex : match.rowIndex + 5, { align: 'auto', behavior: 'smooth' });
});
return nextIndex;
});
},
[handleNavigateToColumn, searchMatches],
);
const handleSearchKeyDown = useCallback(
(event) => {
if (event.key !== 'Enter') return;
event.preventDefault();
const direction = event.shiftKey ? -1 : 1;
const inputValue = event.target.value;
if (normalizeFilterValue(inputValue) !== normalizeFilterValue(globalFilter)) {
handleGlobalFilterChange.clear();
setGlobalFilter(inputValue);
setPendingSearchDirection(direction);
return;
}
navigateToSearchMatch(direction);
},
[globalFilter, handleGlobalFilterChange, navigateToSearchMatch],
);
useEffect(() => {
setSearchMatchIndex(-1);
setActiveSearchMatch(null);
}, [globalFilter]);
useEffect(() => {
if (!pendingSearchDirection) return;
navigateToSearchMatch(pendingSearchDirection);
setPendingSearchDirection(0);
}, [navigateToSearchMatch, pendingSearchDirection]);
useEffect(() => { useEffect(() => {
if (!dataEditingCells?.line_id) { if (!dataEditingCells?.line_id) {
setEditingCell(null); setEditingCell(null);
@ -623,6 +754,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
columnsCurStage={columnsCurStage} columnsCurStage={columnsCurStage}
onChangeSizeMult={setSizeMult} onChangeSizeMult={setSizeMult}
onGlobalFilterChange={handleGlobalFilterChange} onGlobalFilterChange={handleGlobalFilterChange}
globalFilter={globalFilter}
onSearchKeyDown={handleSearchKeyDown}
onSearchNext={() => navigateToSearchMatch(1)}
onSearchPrevious={() => navigateToSearchMatch(-1)}
searchMatchCount={searchMatches.length}
searchMatchIndex={searchMatchIndex}
sizeMult={sizeMult} sizeMult={sizeMult}
onChangeShowColumnFilters={setShowColumnFilters} onChangeShowColumnFilters={setShowColumnFilters}
showColumnFilters={showColumnFilters} showColumnFilters={showColumnFilters}

View File

@ -32,6 +32,32 @@ const controlSx = {
}; };
const searchSx = { height: '2.5rem' }; const searchSx = { height: '2.5rem' };
const searchNavigationSx = {
display: 'inline-flex',
alignItems: 'center',
height: '2rem',
border: '1px solid rgba(0, 0, 0, 0.12)',
borderRadius: '0.4rem',
overflow: 'hidden',
backgroundColor: '#fff',
};
const searchNavigationButtonSx = {
width: '1.65rem',
height: '2rem',
minWidth: '1.65rem',
padding: 0,
borderRadius: 0,
fontSize: '0.95rem',
};
const searchMatchCounterSx = {
minWidth: '2.7rem',
padding: '0 0.3rem',
fontSize: '0.7rem',
lineHeight: 1,
textAlign: 'center',
whiteSpace: 'nowrap',
color: 'rgba(0, 0, 0, 0.65)',
};
const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' }; const exportButtonSx = { height: '2.5rem', minHeight: '2.5rem' };
const SettingPanel = ({ const SettingPanel = ({
@ -46,6 +72,12 @@ const SettingPanel = ({
columnsCurStage, columnsCurStage,
onChangeSizeMult, onChangeSizeMult,
onGlobalFilterChange, onGlobalFilterChange,
globalFilter,
onSearchKeyDown,
onSearchNext,
onSearchPrevious,
searchMatchCount,
searchMatchIndex,
sizeMult, sizeMult,
onChangeShowColumnFilters, onChangeShowColumnFilters,
showColumnFilters, showColumnFilters,
@ -218,7 +250,40 @@ const SettingPanel = ({
</GroupByObject> </GroupByObject>
<Divider orientation='vertical' flexItem /> <Divider orientation='vertical' flexItem />
<GroupByObject title='Поиск'> <GroupByObject title='Поиск'>
<SearchComponent onChange={onGlobalFilterChange} height='2.5rem' sx={searchSx} /> <SearchComponent
value={globalFilter}
onChange={onGlobalFilterChange}
onKeyDown={onSearchKeyDown}
height='2.5rem'
sx={searchSx}
/>
<span style={searchNavigationSx}>
<Tooltip title='Предыдущий результат (Shift+Enter)'>
<span>
<IconButton
onClick={onSearchPrevious}
disabled={!searchMatchCount}
sx={searchNavigationButtonSx}
aria-label='Предыдущий результат поиска'>
</IconButton>
</span>
</Tooltip>
<span style={searchMatchCounterSx}>
{searchMatchCount ? `${Math.max(searchMatchIndex + 1, 1)}/${searchMatchCount}` : '0/0'}
</span>
<Tooltip title='Следующий результат (Enter)'>
<span>
<IconButton
onClick={onSearchNext}
disabled={!searchMatchCount}
sx={searchNavigationButtonSx}
aria-label='Следующий результат поиска'>
</IconButton>
</span>
</Tooltip>
</span>
<Tooltip title='Поиск по колонкам'> <Tooltip title='Поиск по колонкам'>
<IconButton <IconButton
variant='outlined' variant='outlined'