Доработка по возврату тестирования 06.11: - у агрегатных строк (ROOT/GROUP/ITEM/SUB_ITEM) нет data.id, ключ ошибки был «правило:колонка:null» — все агрегаты одной колонки схлопывались в одну запись. Ключ строки теперь getValidationRowKey: line id или sort:<sort_order>; - переход к ошибке ищет строку по тому же ключу (раньше для агрегата прерывался, выделение оставалось на предыдущей ячейке); - прокрутка: вместо scrollToIndex(rowIndex + 5, 'auto') — центрирование и scrollIntoView самой ячейки после отрисовки (у последних строк запас +5 обрезался, шапка внутри контейнера не учитывалась — ячейка оставалась ниже видимой области); - названия колонок в плашке и в тосте EditCell — с подставленными годом и единицами (formatHeaderPaths, как в TableHead); - рамка активной ячейки дублируется псевдоэлементом ::after: inset-тень td перекрывалась красной рамкой ошибочной ячейки. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
185 lines
5.5 KiB
JavaScript
185 lines
5.5 KiB
JavaScript
import ChevronLeftIcon from '@mui/icons-material/ChevronLeft';
|
|
import ChevronRightIcon from '@mui/icons-material/ChevronRight';
|
|
import ErrorOutlineIcon from '@mui/icons-material/ErrorOutlineOutlined';
|
|
import { Button, IconButton, ListSubheader, Menu, MenuItem, Stack } from '@mui/material';
|
|
import { memo, useEffect, useMemo, useState } from 'react';
|
|
import { groupValidationErrors } from '../utils/validationUtils';
|
|
|
|
// AURORA-1893: «более выделенный формат контроля за ошибками» — красная плашка
|
|
// вместо прежней бледно-оранжевой.
|
|
const errorColors = {
|
|
bg: '#FDEDED',
|
|
text: '#D32F2F',
|
|
border: '#D32F2F',
|
|
hover: '#FBD5D5',
|
|
};
|
|
|
|
const stepButtonSx = {
|
|
color: errorColors.text,
|
|
border: `1px solid ${errorColors.border}`,
|
|
borderRadius: '0.5rem',
|
|
'&:hover': { bgcolor: errorColors.hover },
|
|
'&.Mui-disabled': { color: errorColors.border, border: `1px solid ${errorColors.border}`, opacity: 0.5 },
|
|
};
|
|
|
|
// Внутри одной группы (правило + колонка) показываем не больше 20 строк,
|
|
// остальные — свёрнутой строкой «и ещё N».
|
|
const MAX_VISIBLE_CELLS_PER_GROUP = 20;
|
|
|
|
const getColumnTitle = (headerPaths, columnId) => headerPaths?.[columnId]?.filter(Boolean).join(' / ') || columnId;
|
|
|
|
const ValidationErrorsAlert = ({ errors = [], onNavigateToError, headerPaths }) => {
|
|
const [anchorEl, setAnchorEl] = useState(null);
|
|
const [activeIndex, setActiveIndex] = useState(0);
|
|
const isOpen = Boolean(anchorEl);
|
|
|
|
const groups = useMemo(() => groupValidationErrors(errors), [errors]);
|
|
|
|
useEffect(() => {
|
|
if (errors.length === 0) {
|
|
setAnchorEl(null);
|
|
setActiveIndex(0);
|
|
return;
|
|
}
|
|
if (activeIndex >= errors.length) {
|
|
setActiveIndex(0);
|
|
}
|
|
}, [errors, activeIndex]);
|
|
|
|
if (errors.length === 0) return null;
|
|
|
|
const navigateToIndex = (index) => {
|
|
const error = errors[index];
|
|
if (!error) return;
|
|
setActiveIndex(index);
|
|
onNavigateToError?.({ rowKey: error.rowKey, columnId: error.columnId });
|
|
};
|
|
|
|
const handleStep = (direction) => {
|
|
const nextIndex = (activeIndex + direction + errors.length) % errors.length;
|
|
navigateToIndex(nextIndex);
|
|
};
|
|
|
|
const handleSelectCell = (cell) => {
|
|
const index = errors.findIndex((candidate) => candidate.id === cell.id);
|
|
navigateToIndex(index < 0 ? 0 : index);
|
|
setAnchorEl(null);
|
|
};
|
|
|
|
return (
|
|
<>
|
|
<Stack direction='row' sx={{ alignItems: 'center', gap: 0.5 }}>
|
|
<IconButton size='small' onClick={() => handleStep(-1)} aria-label='Предыдущая ошибка' sx={stepButtonSx}>
|
|
<ChevronLeftIcon fontSize='small' />
|
|
</IconButton>
|
|
|
|
<Button
|
|
variant='outlined'
|
|
onClick={(e) => setAnchorEl(e.currentTarget)}
|
|
sx={{
|
|
maxWidth: 360,
|
|
height: '2.5rem',
|
|
borderRadius: '0.5rem',
|
|
bgcolor: errorColors.bg,
|
|
color: errorColors.text,
|
|
borderColor: errorColors.border,
|
|
textTransform: 'none',
|
|
fontWeight: 600,
|
|
justifyContent: 'flex-start',
|
|
px: 1.5,
|
|
gap: 1,
|
|
'&:hover': {
|
|
bgcolor: errorColors.hover,
|
|
borderColor: errorColors.border,
|
|
},
|
|
}}>
|
|
<ErrorOutlineIcon sx={{ fontSize: '1.1rem', flexShrink: 0 }} />
|
|
<span
|
|
style={{
|
|
overflow: 'hidden',
|
|
textOverflow: 'ellipsis',
|
|
whiteSpace: 'nowrap',
|
|
textAlign: 'left',
|
|
}}>
|
|
Ошибок: {errors.length}
|
|
</span>
|
|
</Button>
|
|
|
|
<IconButton size='small' onClick={() => handleStep(1)} aria-label='Следующая ошибка' sx={stepButtonSx}>
|
|
<ChevronRightIcon fontSize='small' />
|
|
</IconButton>
|
|
</Stack>
|
|
|
|
<Menu
|
|
anchorEl={anchorEl}
|
|
open={isOpen}
|
|
onClose={() => setAnchorEl(null)}
|
|
anchorOrigin={{ vertical: 'bottom', horizontal: 'left' }}
|
|
transformOrigin={{ vertical: 'top', horizontal: 'left' }}
|
|
slotProps={{
|
|
paper: {
|
|
'data-validation-errors-menu': true,
|
|
sx: {
|
|
mt: 0.5,
|
|
bgcolor: errorColors.bg,
|
|
color: errorColors.text,
|
|
border: `1px solid ${errorColors.border}`,
|
|
borderRadius: '0.5rem',
|
|
boxShadow: 'none',
|
|
minWidth: anchorEl?.offsetWidth,
|
|
maxWidth: 420,
|
|
maxHeight: 420,
|
|
},
|
|
},
|
|
}}>
|
|
{groups.flatMap((group) => {
|
|
const columnTitle = getColumnTitle(headerPaths, group.columnId);
|
|
const visibleCells = group.cells.slice(0, MAX_VISIBLE_CELLS_PER_GROUP);
|
|
const hiddenCount = group.count - visibleCells.length;
|
|
|
|
return [
|
|
<ListSubheader
|
|
key={`${group.id}-header`}
|
|
sx={{
|
|
bgcolor: 'inherit',
|
|
color: errorColors.text,
|
|
fontWeight: 600,
|
|
lineHeight: 1.3,
|
|
whiteSpace: 'normal',
|
|
py: 0.5,
|
|
}}>
|
|
{group.message} · {columnTitle} ({group.count})
|
|
</ListSubheader>,
|
|
...visibleCells.map((cell) => (
|
|
<MenuItem
|
|
key={cell.id}
|
|
selected={errors[activeIndex]?.id === cell.id}
|
|
onClick={() => handleSelectCell(cell)}
|
|
sx={{
|
|
whiteSpace: 'normal',
|
|
color: errorColors.text,
|
|
cursor: 'pointer',
|
|
pl: 3,
|
|
'&:hover': {
|
|
bgcolor: errorColors.hover,
|
|
},
|
|
}}>
|
|
{cell.rowLabel}
|
|
</MenuItem>
|
|
)),
|
|
...(hiddenCount > 0
|
|
? [
|
|
<MenuItem key={`${group.id}-more`} disabled sx={{ pl: 3, color: errorColors.text, opacity: 0.7 }}>
|
|
и ещё {hiddenCount}
|
|
</MenuItem>,
|
|
]
|
|
: []),
|
|
];
|
|
})}
|
|
</Menu>
|
|
</>
|
|
);
|
|
};
|
|
|
|
export default memo(ValidationErrorsAlert);
|