- validationUtils.collectInvalidErrors теперь собирает ошибки по ЯЧЕЙКАМ
(id/ruleId/columnId/lineId/rowLabel/message), а не одну запись на колонку;
новая groupValidationErrors(errors) группирует их обратно по правилу+колонке
со счётчиком для меню плашки.
- useValidationRules: getCellErrors(columnId, value, row) отдаёт причины
конкретной ячейки (для подсказки), isCellInvalid выражен через него;
сравнение стабильности errors — по id (он уже кодирует правило+колонку+строку).
- RealtimeTable: handleNavigateToError({lineId, columnId}) — находит строку по
getRowId в table.getRowModel().flatRows (не .rows, т.к. flatRows не фильтруется
по свёрнутым группам), раскрывает родителей через setExpanded, включает
скрытую колонку через handleSetColumnsVisibility, выделяет ячейку как
activeSearchMatch и скроллит к ней — по образцу navigateToSearchMatch.
- ValidationErrorsAlert: красная плашка (#FDEDED/#D32F2F) со счётчиком
«Ошибок: N», кнопками ‹› для прохода по всем ошибкам и меню, сгруппированным
по правилу+колонке (до 20 строк, дальше «и ещё N»); перенесена в начало
SettingPanel, перед группами настроек.
- tableColumns/Cell: invalidMessage (из getCellErrors) доходит до Cell и
показывается подсказкой (MUI Tooltip) при наведении на невалидную ячейку.
Тесты (node --test .../validationUtils.test.js): 12/12, включая 5 существующих
тестов nonNegativeRule из AURORA-1892 (не менялись, подтверждают отсутствие
регрессии) и 7 новых для collectInvalidErrors/groupValidationErrors.
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
100 lines
3.7 KiB
JavaScript
100 lines
3.7 KiB
JavaScript
import { getDataRowId } from './rowUtils.js';
|
||
|
||
// Достаёт вложенное поле из объекта строки таблицы
|
||
export const getValueByPath = (row, path) => path.split('.').reduce((value, key) => value?.[key], row);
|
||
|
||
const matchesValidationScope = (scope, { formType, sheetName }) =>
|
||
(!scope.formType || scope.formType === formType) &&
|
||
(!scope.sheetName || scope.sheetName === sheetName);
|
||
|
||
// Пустой includedScopes означает «работает везде»; исключения приоритетнее.
|
||
export const isValidationRuleEnabled = (rule, context = {}) => {
|
||
const includedScopes = rule.includedScopes || [];
|
||
const excludedScopes = rule.excludedScopes || [];
|
||
|
||
if (excludedScopes.some((scope) => matchesValidationScope(scope, context))) return false;
|
||
if (includedScopes.length && !includedScopes.some((scope) => matchesValidationScope(scope, context))) return false;
|
||
|
||
return true;
|
||
};
|
||
|
||
// Строит пути заголовков колонок
|
||
export const buildColumnHeaderPaths = (columns = [], path = [], map = {}) => {
|
||
for (const column of columns) {
|
||
const nextPath = column.header ? [...path, column.header] : path;
|
||
|
||
if (column.columns?.length) {
|
||
buildColumnHeaderPaths(column.columns, nextPath, map);
|
||
} else if (column.accessorKey) {
|
||
map[column.accessorKey] = nextPath;
|
||
}
|
||
}
|
||
|
||
return map;
|
||
};
|
||
|
||
// AURORA-1893: человекочитаемая подпись строки для меню плашки ошибок —
|
||
// название статьи/ВСП, если есть, иначе номер строки (колонка «№»).
|
||
const getRowLabel = (row) => {
|
||
const data = row?.data;
|
||
if (data?.header?.name) return data.header.name;
|
||
if (data?.name) return data.name;
|
||
if (row?.sort_order !== undefined && row?.sort_order !== null) return String(row.sort_order);
|
||
return String(getDataRowId(data) ?? '');
|
||
};
|
||
|
||
// Обходит строки и собирает ошибки по правилу валидации — по записи на каждую
|
||
// ошибочную ЯЧЕЙКУ (не одна запись на колонку, как было раньше), чтобы плашка
|
||
// могла показать счётчик и перейти к конкретной строке.
|
||
export const collectInvalidErrors = (rows, rule, errors, seen) => {
|
||
for (const row of rows) {
|
||
if (!rule.appliesToRow || rule.appliesToRow(row)) {
|
||
for (const columnKey of rule.errorColumnKeys || rule.columnKeys) {
|
||
if (!rule.isInvalid(getValueByPath(row, columnKey), row, columnKey)) continue;
|
||
|
||
const lineId = getDataRowId(row.data);
|
||
const errorId = `${rule.id}:${columnKey}:${lineId}`;
|
||
if (seen.has(errorId)) continue;
|
||
seen.add(errorId);
|
||
|
||
errors.push({
|
||
id: errorId,
|
||
ruleId: rule.id,
|
||
columnId: columnKey,
|
||
lineId,
|
||
rowLabel: getRowLabel(row),
|
||
message: rule.message,
|
||
});
|
||
}
|
||
}
|
||
|
||
collectInvalidErrors(row.subRows || [], rule, errors, seen);
|
||
}
|
||
};
|
||
|
||
// AURORA-1893: группирует ошибки по «правило + колонка» (как раньше отображалось
|
||
// в плашке) и прикладывает к каждой группе список конкретных ячеек со счётчиком.
|
||
export const groupValidationErrors = (errors) => {
|
||
const groupsByKey = new Map();
|
||
|
||
for (const error of errors) {
|
||
const key = `${error.ruleId}:${error.columnId}`;
|
||
let group = groupsByKey.get(key);
|
||
if (!group) {
|
||
group = {
|
||
id: key,
|
||
ruleId: error.ruleId,
|
||
columnId: error.columnId,
|
||
message: error.message,
|
||
cells: [],
|
||
count: 0,
|
||
};
|
||
groupsByKey.set(key, group);
|
||
}
|
||
group.cells.push(error);
|
||
group.count += 1;
|
||
}
|
||
|
||
return [...groupsByKey.values()];
|
||
};
|