fix(realtime): красная рамка у ошибки ячейки, тост без Server error:undefined (AURORA-1894)
Cell.jsx: порядок спреда стилей — base → editable/not-editable → locked →
invalid (INVALID_STYLES последним), чтобы красная рамка 2px не перетиралась
EDITABLE_STYLES у вводимых ячеек с ошибкой.
utils/wsErrorUtils.js: getWsErrorMessage(error) — строка возвращается как
есть, объект берёт message || detail, известные технические префиксы
(computed_field:, role_not_allowed:, window_closed:, org_not_assigned)
заменяются понятным текстом, прочее отдаётся без "Server error:".
RealtimeContext.js: toast.error(getWsErrorMessage(data.error)) вместо
`Server error:${data.error.message}` || '', которое всегда давало undefined
для ошибок-строк (|| не срабатывает на непустой строке).
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
9229ee5ce3
commit
83cb364a9e
@ -181,9 +181,9 @@ const CellComponent = ({ cell, row, column, onClick, globalFilter, columnFilter,
|
||||
textAlign: isNumeric ? 'right' : 'left',
|
||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||
color: textColor,
|
||||
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
|
||||
...(isLocked ? LOCKED_STYLES : {}),
|
||||
...(isInvalid ? INVALID_STYLES : {}),
|
||||
...(isEditable ? EDITABLE_STYLES : NOT_EDITABLE_STYLES),
|
||||
}),
|
||||
[backgroundColor, isInvalid, isLocked, textColor, isEditable, isNumeric, nameIndent],
|
||||
);
|
||||
|
||||
@ -2,6 +2,7 @@ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, use
|
||||
import { toast } from 'react-toastify';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { getRowId } from '../utils/rowUtils';
|
||||
import { getWsErrorMessage } from '../utils/wsErrorUtils';
|
||||
|
||||
const RealtimeActionsContext = createContext(null);
|
||||
const RealtimeConnectionContext = createContext(null);
|
||||
@ -105,8 +106,8 @@ export const RealtimeProvider = ({ children, formId, sheetName, direction, year,
|
||||
const handleMessage = useCallback((data) => {
|
||||
// Обработка ошибок
|
||||
if (data.error) {
|
||||
console.log(data.error.message);
|
||||
toast.error(`Server error:${data.error.message}` || '');
|
||||
console.log(data.error);
|
||||
toast.error(getWsErrorMessage(data.error));
|
||||
onErrorRef.current?.(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
35
web/src/components/RealtimeTable/utils/wsErrorUtils.js
Normal file
35
web/src/components/RealtimeTable/utils/wsErrorUtils.js
Normal 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;
|
||||
};
|
||||
53
web/src/components/RealtimeTable/utils/wsErrorUtils.test.js
Normal file
53
web/src/components/RealtimeTable/utils/wsErrorUtils.test.js
Normal 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'));
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user