fix(formulas): нормализация пробелов выражения перед отправкой и текст ошибки invalid_formula (AURORA-1896, I-2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Andrey Epifancev 2026-11-11 21:06:53 +04:00
parent ecef6eb247
commit e569d18624
5 changed files with 34 additions and 2 deletions

View File

@ -2,7 +2,12 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat
import { toast } from 'react-toastify'; import { toast } from 'react-toastify';
import { nonNegativeColumnKeys } from '../../constants/validation'; import { nonNegativeColumnKeys } from '../../constants/validation';
import { getColumnRequestKey, useCellFormula, useRealtimeActions } from '../../contexts/RealtimeContext'; import { getColumnRequestKey, useCellFormula, useRealtimeActions } from '../../contexts/RealtimeContext';
import { FORMULA_NEEDS_EQUALS_MESSAGE, looksLikeExpressionWithoutEquals, resolveInitialEditorText } from '../../utils/cellFormulaUtils'; import {
FORMULA_NEEDS_EQUALS_MESSAGE,
looksLikeExpressionWithoutEquals,
normalizeFormulaExpression,
resolveInitialEditorText,
} from '../../utils/cellFormulaUtils';
import { import {
calculateFormula, calculateFormula,
extractFormula, extractFormula,
@ -197,7 +202,7 @@ const EditCell = memo(({ refCell, onChange, disabled, value, table, cell }) => {
})); }));
warnIfNegativeNotAllowed(formattedResult); warnIfNegativeNotAllowed(formattedResult);
// AURORA-1896: уходит значение и выражение без «=» — формулу сохраняет сервер. // AURORA-1896: уходит значение и выражение без «=» — формулу сохраняет сервер.
onChange?.(formattedResult, { formula: extractFormula(currentValue) }); onChange?.(formattedResult, { formula: normalizeFormulaExpression(extractFormula(currentValue)) });
return true; return true;
} }

View File

@ -108,3 +108,7 @@ export const clearLegacyFormulaStorage = (storage) => {
return 0; return 0;
} }
}; };
// Сервер принимает в выражении только ASCII-пробел, а клиент при вычислении снимает любые \s
// (NBSP, узкий NBSP, табуляция, перевод строки): перед отправкой убираем все пробельные символы.
export const normalizeFormulaExpression = (expr) => String(expr ?? '').replace(/\s+/g, '');

View File

@ -10,6 +10,7 @@ import {
formulaKey, formulaKey,
isFormulaActual, isFormulaActual,
looksLikeExpressionWithoutEquals, looksLikeExpressionWithoutEquals,
normalizeFormulaExpression,
resolveInitialEditorText, resolveInitialEditorText,
} from './cellFormulaUtils.js'; } from './cellFormulaUtils.js';
@ -159,3 +160,12 @@ test('looksLikeExpressionWithoutEquals', () => {
} }
assert.equal(FORMULA_NEEDS_EQUALS_MESSAGE, 'Формула начинается с ='); assert.equal(FORMULA_NEEDS_EQUALS_MESSAGE, 'Формула начинается с =');
}); });
test('AURORA-1896: normalizeFormulaExpression снимает любые пробельные символы', () => {
assert.equal(normalizeFormulaExpression('1 000+500'), '1000+500');
assert.equal(normalizeFormulaExpression('1\u00a0000+500'), '1000+500');
assert.equal(normalizeFormulaExpression('1\u202f000+5\t0\n0'), '1000+500');
assert.equal(normalizeFormulaExpression('100 + 50'), '100+50');
assert.equal(normalizeFormulaExpression(''), '');
assert.equal(normalizeFormulaExpression(null), '');
});

View File

@ -4,6 +4,10 @@ const KNOWN_PREFIX_MESSAGES = [
{ prefix: 'computed_field:', message: 'Это расчётная ячейка, её нельзя изменить' }, { prefix: 'computed_field:', message: 'Это расчётная ячейка, её нельзя изменить' },
{ prefix: 'role_not_allowed:', message: 'Нет прав на изменение этой колонки' }, { prefix: 'role_not_allowed:', message: 'Нет прав на изменение этой колонки' },
{ prefix: 'window_closed:', message: 'Окно ввода для этой колонки закрыто' }, { prefix: 'window_closed:', message: 'Окно ввода для этой колонки закрыто' },
{
prefix: 'invalid_formula:',
message: 'Формула содержит недопустимые символы, пуста или слишком длинная (до 500 символов)',
},
{ prefix: 'org_not_assigned', message: 'Вы не назначены на эту форму' }, { prefix: 'org_not_assigned', message: 'Вы не назначены на эту форму' },
]; ];

View File

@ -71,3 +71,12 @@ test('AURORA-1978: префикс класса исключения asyncpg ср
); );
assert.equal(getWsErrorMessage("<class 'asyncpg.exceptions.RaiseError'>: "), 'Неизвестная ошибка сервера'); assert.equal(getWsErrorMessage("<class 'asyncpg.exceptions.RaiseError'>: "), 'Неизвестная ошибка сервера');
}); });
test('AURORA-1896: invalid_formula: заменяется человеческим текстом', () => {
const expected = 'Формула содержит недопустимые символы, пуста или слишком длинная (до 500 символов)';
assert.equal(getWsErrorMessage('invalid_formula: chars'), expected);
assert.equal(
getWsErrorMessage({ code: 422, field: '', message: "<class 'asyncpg.exceptions.RaiseError'>: invalid_formula: length" }),
expected,
);
});