Merge pull request 'AURORA-1895: формула из localStorage не перезаписывает чужие значения' (#267) from feat/264-aurora-1895-formula-localstorage into test
This commit is contained in:
commit
38724179c8
@ -2,8 +2,25 @@ 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 { useRealtimeActions } from '../../contexts/RealtimeContext';
|
import { useRealtimeActions } from '../../contexts/RealtimeContext';
|
||||||
import { calculateFormula, extractFormula, formatNumber, isFormula, isSameCellValue, normalizeNumericInput } from '../../utils/formulaUtils';
|
import {
|
||||||
import { loadFromStorage, saveToStorage } from '../../utils/localStorageUtils';
|
calculateFormula,
|
||||||
|
extractFormula,
|
||||||
|
formatNumber,
|
||||||
|
isFormula,
|
||||||
|
isSameCellValue,
|
||||||
|
normalizeNumericInput,
|
||||||
|
resolveEditorState,
|
||||||
|
} from '../../utils/formulaUtils';
|
||||||
|
import { loadFromStorage, removeFromStorage, saveToStorage } from '../../utils/localStorageUtils';
|
||||||
|
|
||||||
|
// Вычисляет отображаемое значение формулы, не бросая исключение наружу.
|
||||||
|
const computeFormulaDisplay = (formulaValue) => {
|
||||||
|
try {
|
||||||
|
return formatNumber(calculateFormula(extractFormula(formulaValue)));
|
||||||
|
} catch {
|
||||||
|
return '#ОШИБКА';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const EDITOR_STYLES = {
|
const EDITOR_STYLES = {
|
||||||
position: 'absolute',
|
position: 'absolute',
|
||||||
@ -33,7 +50,7 @@ const TEXTAREA_STYLES = {
|
|||||||
|
|
||||||
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table, cell }) => {
|
const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, table, cell }) => {
|
||||||
const { endEditing: contextEndEditing } = useRealtimeActions();
|
const { endEditing: contextEndEditing } = useRealtimeActions();
|
||||||
const storageKey = `formula_${tableId}_${cellId}`;
|
const storageKey = `formula_v2_${tableId}_${cellId}`;
|
||||||
|
|
||||||
// AURORA-1891: цифра/Backspace/Delete, нажатые на выделенной (не
|
// AURORA-1891: цифра/Backspace/Delete, нажатые на выделенной (не
|
||||||
// редактируемой пока) ячейке в RealtimeTable.jsx, открывают редактор и
|
// редактируемой пока) ячейке в RealtimeTable.jsx, открывают редактор и
|
||||||
@ -43,14 +60,27 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
// применяется для getSelectedColumnId/getColumnTitle в этом файле.
|
// применяется для getSelectedColumnId/getColumnTitle в этом файле.
|
||||||
// Инициализатор useState вызывается один раз при монтировании — ровно
|
// Инициализатор useState вызывается один раз при монтировании — ровно
|
||||||
// тогда, когда этот редактор открылся именно так.
|
// тогда, когда этот редактор открылся именно так.
|
||||||
|
// hasInitialInputRef: редактор открыт набором символа на выделенной ячейке —
|
||||||
|
// пользователь заменяет значение с нуля, сохранённую формулу подставлять нельзя.
|
||||||
|
const hasInitialInputRef = useRef(false);
|
||||||
const [state, setState] = useState(() => {
|
const [state, setState] = useState(() => {
|
||||||
const initialInput = table?.options?.meta?.getEditorInitialInput?.();
|
const initialInput = table?.options?.meta?.getEditorInitialInput?.();
|
||||||
if (initialInput !== null && initialInput !== undefined) {
|
if (initialInput !== null && initialInput !== undefined) {
|
||||||
|
hasInitialInputRef.current = true;
|
||||||
return { currentValue: initialInput, displayValue: initialInput, isFormulaMode: false };
|
return { currentValue: initialInput, displayValue: initialInput, isFormulaMode: false };
|
||||||
}
|
}
|
||||||
return { currentValue: value, displayValue: value, isFormulaMode: false };
|
return { currentValue: value, displayValue: value, isFormulaMode: false };
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Исходное значение редактора на момент открытия — чтобы не отправлять
|
||||||
|
// на сервер открытие и закрытие ячейки без правки (AURORA-1895).
|
||||||
|
const initialEditorValueRef = useRef(value);
|
||||||
|
// Актуальное значение ячейки для эффекта разрешения формулы: эффект
|
||||||
|
// срабатывает один раз при открытии и не должен затирать набранный текст,
|
||||||
|
// если другой пользователь изменит ячейку, пока она открыта.
|
||||||
|
const valueRef = useRef(value);
|
||||||
|
valueRef.current = value;
|
||||||
|
|
||||||
// Позиция ячейки
|
// Позиция ячейки
|
||||||
const [position, setPosition] = useState();
|
const [position, setPosition] = useState();
|
||||||
const textareaRef = useRef(null);
|
const textareaRef = useRef(null);
|
||||||
@ -61,25 +91,24 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
|
|
||||||
const refFinished = useRef(false);
|
const refFinished = useRef(false);
|
||||||
|
|
||||||
// Загрузка сохраненной формулы
|
// Загрузка сохраненной формулы (AURORA-1895): подставляем её только если
|
||||||
|
// результат совпадает с текущим значением ячейки, иначе формула устарела
|
||||||
|
// (stale: значение изменил другой пользователь) и удаляется из хранилища.
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
if (hasInitialInputRef.current) return;
|
||||||
|
|
||||||
|
const currentCellValue = valueRef.current;
|
||||||
const savedFormula = loadFromStorage(storageKey);
|
const savedFormula = loadFromStorage(storageKey);
|
||||||
if (savedFormula && isFormula(savedFormula)) {
|
const { currentValue, isFormulaMode, stale } = resolveEditorState(savedFormula, currentCellValue);
|
||||||
try {
|
|
||||||
const formula = extractFormula(savedFormula);
|
if (stale) {
|
||||||
const result = calculateFormula(formula);
|
removeFromStorage(storageKey);
|
||||||
setState({
|
}
|
||||||
currentValue: savedFormula,
|
|
||||||
displayValue: formatNumber(result),
|
initialEditorValueRef.current = currentValue;
|
||||||
isFormulaMode: true,
|
|
||||||
});
|
if (isFormulaMode) {
|
||||||
} catch {
|
setState({ currentValue, displayValue: computeFormulaDisplay(currentValue), isFormulaMode: true });
|
||||||
setState({
|
|
||||||
currentValue: savedFormula,
|
|
||||||
displayValue: '#ОШИБКА',
|
|
||||||
isFormulaMode: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}, [storageKey]);
|
}, [storageKey]);
|
||||||
|
|
||||||
@ -157,45 +186,56 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
[cell, table],
|
[cell, table],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Возвращает true, если можно закрывать редактор (сохранено либо не было
|
||||||
|
// изменений), false — если формула некорректна и редактор должен остаться
|
||||||
|
// открытым (ничего не отправляем на сервер в этом случае).
|
||||||
const handleSave = useCallback(() => {
|
const handleSave = useCallback(() => {
|
||||||
const { currentValue } = state;
|
const { currentValue } = state;
|
||||||
|
|
||||||
|
// AURORA-1891/1895: ячейку открыли и закрыли без правки — на сервер
|
||||||
|
// ничего не шлём (ни cell_updated, ни запись в аудит).
|
||||||
|
if (currentValue === initialEditorValueRef.current) return true;
|
||||||
|
|
||||||
if (isFormula(currentValue)) {
|
if (isFormula(currentValue)) {
|
||||||
saveToStorage(storageKey, currentValue);
|
let result;
|
||||||
try {
|
try {
|
||||||
const formula = extractFormula(currentValue);
|
result = calculateFormula(extractFormula(currentValue));
|
||||||
const result = calculateFormula(formula);
|
|
||||||
const formattedResult = formatNumber(result);
|
|
||||||
setState((prev) => ({
|
|
||||||
...prev,
|
|
||||||
displayValue: formattedResult,
|
|
||||||
isFormulaMode: true,
|
|
||||||
}));
|
|
||||||
warnIfNegativeNotAllowed(formattedResult);
|
|
||||||
onChange?.(formattedResult);
|
|
||||||
} catch {
|
} catch {
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayValue: '#ОШИБКА',
|
displayValue: '#ОШИБКА',
|
||||||
isFormulaMode: true,
|
isFormulaMode: true,
|
||||||
}));
|
}));
|
||||||
onChange?.(currentValue);
|
toast.error('Формула не вычисляется, проверьте выражение');
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
} else {
|
|
||||||
localStorage.removeItem(storageKey);
|
const formattedResult = formatNumber(result);
|
||||||
|
saveToStorage(storageKey, currentValue);
|
||||||
setState((prev) => ({
|
setState((prev) => ({
|
||||||
...prev,
|
...prev,
|
||||||
displayValue: currentValue,
|
displayValue: formattedResult,
|
||||||
isFormulaMode: false,
|
isFormulaMode: true,
|
||||||
}));
|
}));
|
||||||
const normalizedValue = normalizeNumericInput(currentValue);
|
warnIfNegativeNotAllowed(formattedResult);
|
||||||
warnIfNegativeNotAllowed(normalizedValue);
|
onChange?.(formattedResult);
|
||||||
// AURORA-1891: значение не изменилось — не шлём onChange (а значит и
|
return true;
|
||||||
// cell_updated): открытие и закрытие ячейки без правки не должно
|
|
||||||
// создавать запись в журнале аудита.
|
|
||||||
if (isSameCellValue(normalizedValue, value)) return;
|
|
||||||
onChange?.(normalizedValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
removeFromStorage(storageKey);
|
||||||
|
setState((prev) => ({
|
||||||
|
...prev,
|
||||||
|
displayValue: currentValue,
|
||||||
|
isFormulaMode: false,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const normalizedValue = normalizeNumericInput(currentValue);
|
||||||
|
warnIfNegativeNotAllowed(normalizedValue);
|
||||||
|
// Значение фактически не изменилось (напр. '150' вместо 150) — не шлём
|
||||||
|
// onChange (а значит и cell_updated).
|
||||||
|
if (isSameCellValue(normalizedValue, value)) return true;
|
||||||
|
onChange?.(normalizedValue);
|
||||||
|
return true;
|
||||||
}, [state, storageKey, onChange, warnIfNegativeNotAllowed, value]);
|
}, [state, storageKey, onChange, warnIfNegativeNotAllowed, value]);
|
||||||
|
|
||||||
// Завершает редактирование текущей ячейки (cell_edit_end) — читает
|
// Завершает редактирование текущей ячейки (cell_edit_end) — читает
|
||||||
@ -212,13 +252,14 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
}
|
}
|
||||||
}, [table, contextEndEditing]);
|
}, [table, contextEndEditing]);
|
||||||
|
|
||||||
|
// Возвращает true, если редактор закрыт; false — если остался открытым
|
||||||
|
// (формула не вычисляется) либо уже был закрыт ранее.
|
||||||
const finishEditing = useCallback(() => {
|
const finishEditing = useCallback(() => {
|
||||||
if (refFinished.current) return;
|
if (refFinished.current) return false;
|
||||||
|
if (!disabled && !handleSave()) return false;
|
||||||
refFinished.current = true;
|
refFinished.current = true;
|
||||||
if (!disabled) {
|
|
||||||
handleSave();
|
|
||||||
}
|
|
||||||
endCurrentEditing();
|
endCurrentEditing();
|
||||||
|
return true;
|
||||||
}, [disabled, handleSave, endCurrentEditing]);
|
}, [disabled, handleSave, endCurrentEditing]);
|
||||||
|
|
||||||
// AURORA-1891: Esc — отмена, handleSave вообще не вызывается (никакого
|
// AURORA-1891: Esc — отмена, handleSave вообще не вызывается (никакого
|
||||||
@ -267,8 +308,11 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
// текущей (см. ruling в PR) — здесь мы только помечаем цель и сохраняем.
|
// текущей (см. ruling в PR) — здесь мы только помечаем цель и сохраняем.
|
||||||
if (e.key === 'Tab') {
|
if (e.key === 'Tab') {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, e.shiftKey ? 'left' : 'right');
|
// AURORA-1895: цель перехода помечаем только если редактор реально
|
||||||
finishEditing();
|
// закрылся — при невычисляемой формуле он остаётся открытым.
|
||||||
|
if (finishEditing()) {
|
||||||
|
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, e.shiftKey ? 'left' : 'right');
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -276,8 +320,9 @@ const EditCell = memo(({ refCell, onChange, disabled, value, tableId, cellId, ta
|
|||||||
// e.preventDefault() не вызывается).
|
// e.preventDefault() не вызывается).
|
||||||
if (e.key === 'Enter' && !e.shiftKey) {
|
if (e.key === 'Enter' && !e.shiftKey) {
|
||||||
e.preventDefault();
|
e.preventDefault();
|
||||||
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, 'down');
|
if (finishEditing()) {
|
||||||
finishEditing();
|
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, 'down');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[cancelEditing, finishEditing, table, cell],
|
[cancelEditing, finishEditing, table, cell],
|
||||||
|
|||||||
@ -28,6 +28,7 @@ import { getColumnVirtualizerOptions, getOrderedVisibleColumns } from './utils/c
|
|||||||
import { isCellEditable } from './utils/cellEditability';
|
import { isCellEditable } from './utils/cellEditability';
|
||||||
import { findNextEditableColumnId, findNextEditableRowId } from './utils/cellNavigation';
|
import { findNextEditableColumnId, findNextEditableRowId } from './utils/cellNavigation';
|
||||||
import { canAddRowForHeader, getRowId } from './utils/rowUtils';
|
import { canAddRowForHeader, getRowId } from './utils/rowUtils';
|
||||||
|
import { clearLegacyFormulaKeys } from './utils/localStorageUtils';
|
||||||
|
|
||||||
import { DictVspApi } from '../../api/dict-vsp';
|
import { DictVspApi } from '../../api/dict-vsp';
|
||||||
import { useAuth } from '../../app/context/AuthProvider';
|
import { useAuth } from '../../app/context/AuthProvider';
|
||||||
@ -224,6 +225,12 @@ const RealtimeTable = ({ formType, formId, sheetName, direction, year }) => {
|
|||||||
columnVirtualizerRef.current?.measure?.();
|
columnVirtualizerRef.current?.measure?.();
|
||||||
}, [sizeMult]);
|
}, [sizeMult]);
|
||||||
|
|
||||||
|
// Разовая очистка устаревших ключей формул, привязанных к позиции строки
|
||||||
|
// (AURORA-1895): они стали недостоверны после перехода на ключи formula_v2_*.
|
||||||
|
useEffect(() => {
|
||||||
|
clearLegacyFormulaKeys();
|
||||||
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!formType || !sheetName) return;
|
if (!formType || !sheetName) return;
|
||||||
let isCancelled = false;
|
let isCancelled = false;
|
||||||
|
|||||||
@ -11,6 +11,7 @@ import { isCellEditable } from './utils/cellEditability';
|
|||||||
import { getColumnValue } from './utils/columnUtils';
|
import { getColumnValue } from './utils/columnUtils';
|
||||||
|
|
||||||
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
import VspDropdownEditCell from './Cell/EditCell/VspDropdownEditCell';
|
||||||
|
import { getRowId } from './utils/rowUtils';
|
||||||
|
|
||||||
const EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable }) => {
|
const EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable }) => {
|
||||||
const { formId, formType, sheetName, direction } = useParams();
|
const { formId, formType, sheetName, direction } = useParams();
|
||||||
@ -54,7 +55,9 @@ const EditCellPortal = React.memo(({ cell, table, EditCell, onError, isEditable
|
|||||||
onChange: handleChange,
|
onChange: handleChange,
|
||||||
tableId: tableKey,
|
tableId: tableKey,
|
||||||
table,
|
table,
|
||||||
cellId: `${cell.row.id}_${cell.column.id}`,
|
// Привязан к line_id из данных строки, а не к её позиции в дереве
|
||||||
|
// (cell.row.id — индекс-путь, который переезжает при вставке/удалении строк).
|
||||||
|
cellId: `${getRowId(cell.row)}_${cell.column.id}`,
|
||||||
}),
|
}),
|
||||||
[cell, isEditable, handleChange, tableKey, table],
|
[cell, isEditable, handleChange, tableKey, table],
|
||||||
);
|
);
|
||||||
|
|||||||
@ -123,6 +123,37 @@ export const isSameCellValue = (a, b) => {
|
|||||||
return normalizeComparableValue(a) === normalizeComparableValue(b);
|
return normalizeComparableValue(a) === normalizeComparableValue(b);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Начальное состояние редактора ячейки.
|
||||||
|
* Решает, показывать ли сохранённую формулу или текущее значение ячейки,
|
||||||
|
* и помечает формулу как устаревшую (stale), если её результат расходится
|
||||||
|
* с текущим значением — значит, значение изменил кто-то другой, либо
|
||||||
|
* формула больше не вычисляется.
|
||||||
|
* @param {string|null} savedFormula - строка вида '=...' из localStorage, либо null
|
||||||
|
* @param {string|number|null} value - текущее значение ячейки (с сервера)
|
||||||
|
* @returns {{ currentValue: string|number|null, isFormulaMode: boolean, stale: boolean }}
|
||||||
|
*/
|
||||||
|
export const resolveEditorState = (savedFormula, value) => {
|
||||||
|
if (!savedFormula || !isFormula(savedFormula)) {
|
||||||
|
return { currentValue: value, isFormulaMode: false, stale: false };
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const expression = extractFormula(savedFormula);
|
||||||
|
const result = calculateFormula(expression);
|
||||||
|
const numericValue = Number(value);
|
||||||
|
const stale = Number.isNaN(numericValue) || Math.abs(result - numericValue) > 1e-9;
|
||||||
|
|
||||||
|
if (stale) {
|
||||||
|
return { currentValue: value, isFormulaMode: false, stale: true };
|
||||||
|
}
|
||||||
|
|
||||||
|
return { currentValue: savedFormula, isFormulaMode: true, stale: false };
|
||||||
|
} catch {
|
||||||
|
return { currentValue: value, isFormulaMode: false, stale: true };
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Форматирует число для отображения
|
* Форматирует число для отображения
|
||||||
* @param {number} value - Число для форматирования
|
* @param {number} value - Число для форматирования
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
import assert from 'node:assert/strict';
|
import assert from 'node:assert/strict';
|
||||||
import test from 'node:test';
|
import test from 'node:test';
|
||||||
import { isSameCellValue } from './formulaUtils.js';
|
import { isSameCellValue, resolveEditorState } from './formulaUtils.js';
|
||||||
|
|
||||||
// AURORA-1891: handleSave (EditCell.jsx) не должен слать cell_updated, если
|
// AURORA-1891: handleSave (EditCell.jsx) не должен слать cell_updated, если
|
||||||
// значение фактически не изменилось — иначе лишняя запись в БД/аудите при
|
// значение фактически не изменилось — иначе лишняя запись в БД/аудите при
|
||||||
@ -36,3 +36,51 @@ test('different values (numeric or text) are not the same', () => {
|
|||||||
test('non-numeric text compares as-is after trimming surrounding whitespace', () => {
|
test('non-numeric text compares as-is after trimming surrounding whitespace', () => {
|
||||||
assert.equal(isSameCellValue(' текст ', 'текст'), true);
|
assert.equal(isSameCellValue(' текст ', 'текст'), true);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// AURORA-1895: формула из localStorage не перезаписывает чужое значение.
|
||||||
|
|
||||||
|
test('resolveEditorState keeps formula mode when the formula result matches the current value', () => {
|
||||||
|
const result = resolveEditorState('=100+50', 150);
|
||||||
|
assert.equal(result.isFormulaMode, true);
|
||||||
|
assert.equal(result.stale, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveEditorState marks the formula stale when the current value diverged (another user changed it)', () => {
|
||||||
|
const result = resolveEditorState('=100+50', 200);
|
||||||
|
assert.equal(result.stale, true);
|
||||||
|
assert.equal(result.currentValue, 200);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveEditorState keeps formula mode when the current value is a numeric string that matches', () => {
|
||||||
|
const result = resolveEditorState('=100+50', '150');
|
||||||
|
assert.equal(result.stale, false);
|
||||||
|
assert.equal(result.isFormulaMode, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveEditorState marks the formula stale when it cannot be calculated', () => {
|
||||||
|
const result = resolveEditorState('=abc', 0);
|
||||||
|
assert.equal(result.stale, true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('resolveEditorState falls back to plain value mode when there is no saved formula', () => {
|
||||||
|
const result = resolveEditorState(null, 5);
|
||||||
|
assert.equal(result.isFormulaMode, false);
|
||||||
|
assert.equal(result.stale, false);
|
||||||
|
assert.equal(result.currentValue, 5);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isSameCellValue treats a numeric string and a number as equal', () => {
|
||||||
|
assert.equal(isSameCellValue('150', 150), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isSameCellValue treats an empty string and null as equal', () => {
|
||||||
|
assert.equal(isSameCellValue('', null), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isSameCellValue treats a numeric string with a trailing .0 as equal to the plain number', () => {
|
||||||
|
assert.equal(isSameCellValue('150.0', 150), true);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('isSameCellValue treats different numbers as not equal', () => {
|
||||||
|
assert.equal(isSameCellValue('151', 150), false);
|
||||||
|
});
|
||||||
|
|||||||
@ -81,6 +81,23 @@ export const removeFromStorage = (key) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Удаляет устаревшие ключи формул вида `formula_<tableId>_<cellId>`,
|
||||||
|
* привязанные к позиции строки в дереве таблицы (AURORA-1895). Текущие ключи
|
||||||
|
* формул имеют префикс `formula_v2_` и привязаны к line_id — их не трогаем.
|
||||||
|
* @returns {number} - количество удалённых ключей
|
||||||
|
*/
|
||||||
|
export const clearLegacyFormulaKeys = () => {
|
||||||
|
try {
|
||||||
|
const keysToRemove = Object.keys(localStorage).filter((key) => key.startsWith('formula_') && !key.startsWith('formula_v2_'));
|
||||||
|
keysToRemove.forEach((key) => localStorage.removeItem(key));
|
||||||
|
return keysToRemove.length;
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error clearing legacy formula keys from localStorage:', error);
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Очистка всех данных приложения из localStorage
|
* Очистка всех данных приложения из localStorage
|
||||||
* @param {string[]} keysToKeep - Массив ключей, которые нужно сохранить
|
* @param {string[]} keysToKeep - Массив ключей, которые нужно сохранить
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user