Merge pull request 'AURORA-1894: красная рамка у ошибки ячейки, тост без Server error:undefined' (#277) from feat/275-aurora-1894-error-border-toast into test

This commit is contained in:
aep 2026-11-02 20:34:38 +03:00
commit 5fe1264064
4 changed files with 92 additions and 3 deletions

View File

@ -200,9 +200,9 @@ const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter,
textAlign: isNumeric ? 'right' : 'left', textAlign: isNumeric ? 'right' : 'left',
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor, backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
color: textColor, color: textColor,
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
...(isLocked ? LOCKED_STYLES : {}), ...(isLocked ? LOCKED_STYLES : {}),
...(isInvalid ? INVALID_STYLES : {}), ...(isInvalid ? INVALID_STYLES : {}),
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
}), }),
[backgroundColor, isInvalid, isLocked, textColor, isEditable, isNumeric, nameIndent], [backgroundColor, isInvalid, isLocked, textColor, isEditable, isNumeric, nameIndent],
); );

View File

@ -3,6 +3,7 @@ import { toast } from 'react-toastify';
import { useWebSocket } from '../hooks/useWebSocket'; import { useWebSocket } from '../hooks/useWebSocket';
import { buildCellUpdatedMessage } from '../utils/messageUtils'; import { buildCellUpdatedMessage } from '../utils/messageUtils';
import { getRowId } from '../utils/rowUtils'; import { getRowId } from '../utils/rowUtils';
import { getWsErrorMessage } from '../utils/wsErrorUtils';
const RealtimeActionsContext = createContext(null); const RealtimeActionsContext = createContext(null);
const RealtimeConnectionContext = createContext(null); const RealtimeConnectionContext = createContext(null);
@ -106,8 +107,8 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
const handleMessage = useCallback((data) => { const handleMessage = useCallback((data) => {
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
console.log(data.error.message); console.log(data.error);
toast.error(`Server error:${data.error.message}` || ''); toast.error(getWsErrorMessage(data.error));
onErrorRef.current?.(data.error); onErrorRef.current?.(data.error);
return; return;
} }

View File

@ -0,0 +1,35 @@
// Человеческие тексты для технических префиксов ошибок, которые шлёт backend
// (api/src/core/CONSTANTS.py: VALIDATION_PREFIXES, PG_403_ERRORS, и org_not_assigned из прав доступа).
const KNOWN_PREFIX_MESSAGES = [
{ prefix: 'computed_field:', message: 'Это расчётная ячейка, её нельзя изменить' },
{ prefix: 'role_not_allowed:', message: 'Нет прав на изменение этой колонки' },
{ prefix: 'window_closed:', message: 'Окно ввода для этой колонки закрыто' },
{ prefix: 'org_not_assigned', message: 'Вы не назначены на эту форму' },
];
const DEFAULT_ERROR_MESSAGE = 'Неизвестная ошибка сервера';
// Если текст начинается с известного технического префикса — возвращает человеческую замену, иначе null.
const resolveKnownPrefix = (text) => {
const normalized = String(text ?? '').trim();
if (!normalized) return null;
const lower = normalized.toLowerCase();
const match = KNOWN_PREFIX_MESSAGES.find(({ prefix }) => lower.startsWith(prefix));
return match ? match.message : null;
};
// Приводит ошибку WebSocket (строка или объект {code, field, message}/{detail}) к читаемому тексту для тоста.
// Никогда не возвращает undefined/null и не добавляет технический префикс "Server error:".
export const getWsErrorMessage = (error) => {
if (typeof error === 'string') {
return resolveKnownPrefix(error) ?? (error.trim() || DEFAULT_ERROR_MESSAGE);
}
if (error && typeof error === 'object') {
const raw = error.message || error.detail;
if (!raw) return DEFAULT_ERROR_MESSAGE;
return resolveKnownPrefix(raw) ?? raw;
}
return DEFAULT_ERROR_MESSAGE;
};

View File

@ -0,0 +1,53 @@
import assert from 'node:assert/strict';
import { test } from 'node:test';
import { getWsErrorMessage } from './wsErrorUtils.js';
test('строка возвращается как есть', () => {
assert.equal(getWsErrorMessage('Ячейка уже редактируется другим пользователем'), 'Ячейка уже редактируется другим пользователем');
});
test('объект с message возвращает message', () => {
assert.equal(getWsErrorMessage({ code: 400, field: 'q1.transfer_econ', message: 'Нельзя указать отрицательное значение' }), 'Нельзя указать отрицательное значение');
});
test('объект без message возвращает detail', () => {
assert.equal(getWsErrorMessage({ detail: 'Ошибка подключения к базе данных' }), 'Ошибка подключения к базе данных');
});
test('объект без message и без detail возвращает дефолтный текст, а не undefined', () => {
const result = getWsErrorMessage({ code: 500 });
assert.notEqual(result, undefined);
assert.equal(typeof result, 'string');
assert.ok(!result.includes('undefined'));
});
test('префикс computed_field: заменяется человеческим текстом', () => {
assert.equal(getWsErrorMessage('computed_field: q1.corrected_plan'), 'Это расчётная ячейка, её нельзя изменить');
assert.equal(getWsErrorMessage({ message: 'computed_field: q1.corrected_plan' }), 'Это расчётная ячейка, её нельзя изменить');
});
test('префикс role_not_allowed: заменяется человеческим текстом', () => {
assert.equal(getWsErrorMessage('role_not_allowed: data.q1.transfer_econ'), 'Нет прав на изменение этой колонки');
});
test('префикс window_closed: заменяется человеческим текстом', () => {
assert.equal(getWsErrorMessage('window_closed: data.q1.transfer_econ'), 'Окно ввода для этой колонки закрыто');
});
test('префикс org_not_assigned заменяется человеческим текстом', () => {
assert.equal(getWsErrorMessage('org_not_assigned'), 'Вы не назначены на эту форму');
assert.equal(getWsErrorMessage({ message: 'org_not_assigned: form_id=123' }), 'Вы не назначены на эту форму');
});
test('неизвестный текст возвращается без технического префикса Server error:', () => {
const result = getWsErrorMessage('Database unavailable');
assert.equal(result, 'Database unavailable');
assert.ok(!result.startsWith('Server error:'));
});
test('пустая строка и отсутствующая ошибка не превращаются в undefined', () => {
assert.notEqual(getWsErrorMessage(''), undefined);
assert.notEqual(getWsErrorMessage(null), undefined);
assert.notEqual(getWsErrorMessage(undefined), undefined);
assert.ok(!String(getWsErrorMessage(undefined)).includes('undefined'));
});