From e569d18624c61e720b9dd923ace8fab33f38cf2c Mon Sep 17 00:00:00 2001 From: Andrey Epifancev Date: Wed, 11 Nov 2026 21:06:53 +0400 Subject: [PATCH] =?UTF-8?q?fix(formulas):=20=D0=BD=D0=BE=D1=80=D0=BC=D0=B0?= =?UTF-8?q?=D0=BB=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=BF=D1=80=D0=BE?= =?UTF-8?q?=D0=B1=D0=B5=D0=BB=D0=BE=D0=B2=20=D0=B2=D1=8B=D1=80=D0=B0=D0=B6?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=BF=D0=B5=D1=80=D0=B5=D0=B4=20?= =?UTF-8?q?=D0=BE=D1=82=D0=BF=D1=80=D0=B0=D0=B2=D0=BA=D0=BE=D0=B9=20=D0=B8?= =?UTF-8?q?=20=D1=82=D0=B5=D0=BA=D1=81=D1=82=20=D0=BE=D1=88=D0=B8=D0=B1?= =?UTF-8?q?=D0=BA=D0=B8=20invalid=5Fformula=20(AURORA-1896,=20I-2)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 5.5 --- .../RealtimeTable/Cell/EditCell/EditCell.jsx | 9 +++++++-- .../components/RealtimeTable/utils/cellFormulaUtils.js | 4 ++++ .../RealtimeTable/utils/cellFormulaUtils.test.js | 10 ++++++++++ web/src/components/RealtimeTable/utils/wsErrorUtils.js | 4 ++++ .../RealtimeTable/utils/wsErrorUtils.test.js | 9 +++++++++ 5 files changed, 34 insertions(+), 2 deletions(-) diff --git a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx index 2dd9d03..07788cf 100644 --- a/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx +++ b/web/src/components/RealtimeTable/Cell/EditCell/EditCell.jsx @@ -2,7 +2,12 @@ import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useStat import { toast } from 'react-toastify'; import { nonNegativeColumnKeys } from '../../constants/validation'; 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 { calculateFormula, extractFormula, @@ -197,7 +202,7 @@ const EditCell = memo(({ refCell, onChange, disabled, value, table, cell }) => { })); warnIfNegativeNotAllowed(formattedResult); // AURORA-1896: уходит значение и выражение без «=» — формулу сохраняет сервер. - onChange?.(formattedResult, { formula: extractFormula(currentValue) }); + onChange?.(formattedResult, { formula: normalizeFormulaExpression(extractFormula(currentValue)) }); return true; } diff --git a/web/src/components/RealtimeTable/utils/cellFormulaUtils.js b/web/src/components/RealtimeTable/utils/cellFormulaUtils.js index a7b7174..26c70f3 100644 --- a/web/src/components/RealtimeTable/utils/cellFormulaUtils.js +++ b/web/src/components/RealtimeTable/utils/cellFormulaUtils.js @@ -108,3 +108,7 @@ export const clearLegacyFormulaStorage = (storage) => { return 0; } }; + +// Сервер принимает в выражении только ASCII-пробел, а клиент при вычислении снимает любые \s +// (NBSP, узкий NBSP, табуляция, перевод строки): перед отправкой убираем все пробельные символы. +export const normalizeFormulaExpression = (expr) => String(expr ?? '').replace(/\s+/g, ''); diff --git a/web/src/components/RealtimeTable/utils/cellFormulaUtils.test.js b/web/src/components/RealtimeTable/utils/cellFormulaUtils.test.js index f871eb3..cec6c6a 100644 --- a/web/src/components/RealtimeTable/utils/cellFormulaUtils.test.js +++ b/web/src/components/RealtimeTable/utils/cellFormulaUtils.test.js @@ -10,6 +10,7 @@ import { formulaKey, isFormulaActual, looksLikeExpressionWithoutEquals, + normalizeFormulaExpression, resolveInitialEditorText, } from './cellFormulaUtils.js'; @@ -159,3 +160,12 @@ test('looksLikeExpressionWithoutEquals', () => { } 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), ''); +}); diff --git a/web/src/components/RealtimeTable/utils/wsErrorUtils.js b/web/src/components/RealtimeTable/utils/wsErrorUtils.js index e8def58..aef7283 100644 --- a/web/src/components/RealtimeTable/utils/wsErrorUtils.js +++ b/web/src/components/RealtimeTable/utils/wsErrorUtils.js @@ -4,6 +4,10 @@ const KNOWN_PREFIX_MESSAGES = [ { prefix: 'computed_field:', message: 'Это расчётная ячейка, её нельзя изменить' }, { prefix: 'role_not_allowed:', message: 'Нет прав на изменение этой колонки' }, { prefix: 'window_closed:', message: 'Окно ввода для этой колонки закрыто' }, + { + prefix: 'invalid_formula:', + message: 'Формула содержит недопустимые символы, пуста или слишком длинная (до 500 символов)', + }, { prefix: 'org_not_assigned', message: 'Вы не назначены на эту форму' }, ]; diff --git a/web/src/components/RealtimeTable/utils/wsErrorUtils.test.js b/web/src/components/RealtimeTable/utils/wsErrorUtils.test.js index 1dbbda0..2cc4735 100644 --- a/web/src/components/RealtimeTable/utils/wsErrorUtils.test.js +++ b/web/src/components/RealtimeTable/utils/wsErrorUtils.test.js @@ -71,3 +71,12 @@ test('AURORA-1978: префикс класса исключения asyncpg ср ); assert.equal(getWsErrorMessage(": "), 'Неизвестная ошибка сервера'); }); + +test('AURORA-1896: invalid_formula: заменяется человеческим текстом', () => { + const expected = 'Формула содержит недопустимые символы, пуста или слишком длинная (до 500 символов)'; + assert.equal(getWsErrorMessage('invalid_formula: chars'), expected); + assert.equal( + getWsErrorMessage({ code: 422, field: '', message: ": invalid_formula: length" }), + expected, + ); +});