Andrey Epifancev 8558670b46 feat(table): редактирование одним кликом и с клавиатуры, без лишних записей в аудит (AURORA-1891)
- processColumns (tableColumns.jsx): enableEditing для каждой листовой колонки
  через isCellEditable (PR #281), а не только для vsp_dropdown — MRT больше не
  открывает редактор (даже disabled) на нередактируемой ячейке, сервер не берёт
  блокировку, коллеги не видят 🔒.
- Одиночный клик по редактируемой, не заблокированной ячейке открывает
  редактор (muiTableBodyCellProps.onClick, RealtimeTable.jsx); клик по
  нередактируемой/занятой ячейке, как раньше, выделяет строку. Удалён мёртвый
  onClick/handleClick из Cell.jsx.
- Клавиатура на выделенной ячейке: Enter/F2 — открыть редактор; Delete/Backspace
  — открыть с пустым значением; печатный символ — открыть и подставить его
  вместо текущего значения (хранится в state RealtimeTable, читается через
  table.options.meta.getEditorInitialInput).
- EditCell.jsx/VspDropdownEditCell.jsx: Esc — отмена без cell_updated; Tab/
  Shift+Tab и Enter — сохранить и перейти к следующей/предыдущей редактируемой
  ячейке через table.options.meta.openNextEditableCell → utils/cellNavigation.js
  (чистые findNextEditableColumnId/findNextEditableRowId по
  getOrderedVisibleColumns и table.getRowModel().flatRows).
- utils/formulaUtils.js: isSameCellValue — handleSave не шлёт cell_updated,
  если значение не изменилось (число/строка, «1 500,0», пусто/null); формула
  всегда считается изменённой.
- Фокус: setTimeout(50) → requestAnimationFrame; после закрытия редактора
  фокус возвращается в <td> ячейки.

Тесты: utils/cellNavigation.test.js, utils/formulaUtils.test.js (новые,
15 тестов). node --test src/components/RealtimeTable/utils/*.test.js:
85 тестов, 84 passed, 1 известный pre-existing fail (columnLayoutUtils.test.js,
нет node_modules в этом worktree, см. PR #281/#283).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-11-01 16:14:02 +04:00

79 lines
4.1 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import assert from 'node:assert/strict';
import test from 'node:test';
import { findNextEditableColumnId, findNextEditableRowId } from './cellNavigation.js';
// AURORA-1891: Tab/Shift+Tab (в строке) и Enter (по столбцу, вниз/вверх) —
// EditCell.jsx/VspDropdownEditCell.jsx знают только текущую строку/колонку,
// а сам обход видимых листовых колонок (getOrderedVisibleColumns) и строк
// (table.getRowModel().flatRows) и предикат редактируемости (isCellEditable)
// собирает вызывающая сторона (RealtimeTable.jsx meta.openNextEditableCell).
// Здесь — только чистый обход уже готового списка.
// columns — то, что вернул бы getOrderedVisibleColumns: скрытая колонка
// конфига сюда вообще не попала бы, поэтому «пропуск скрытых» проверяется
// самим фактом, что функция работает только с тем, что ей передали, и про
// скрытые колонки ничего не знает.
const COLUMNS = [
{ id: 'a', editable: true },
{ id: 'b', editable: false }, // не редактируется в этой строке (напр. вычисляемая)
{ id: 'c', editable: true },
{ id: 'd', editable: false },
];
const isColumnEditable = (column) => column.editable;
test('Tab: moves right to the next editable column, skipping non-editable ones', () => {
const next = findNextEditableColumnId({ columns: COLUMNS, currentColumnId: 'a', step: 1, isEditable: isColumnEditable });
assert.equal(next, 'c');
});
test('Shift+Tab: moves left to the previous editable column', () => {
const prev = findNextEditableColumnId({ columns: COLUMNS, currentColumnId: 'c', step: -1, isEditable: isColumnEditable });
assert.equal(prev, 'a');
});
test('Tab at the end of the row returns null (конец строки)', () => {
const next = findNextEditableColumnId({ columns: COLUMNS, currentColumnId: 'c', step: 1, isEditable: isColumnEditable });
assert.equal(next, null);
});
test('Shift+Tab at the start of the row returns null', () => {
const prev = findNextEditableColumnId({ columns: COLUMNS, currentColumnId: 'a', step: -1, isEditable: isColumnEditable });
assert.equal(prev, null);
});
test('an unknown current column id returns null instead of throwing', () => {
const next = findNextEditableColumnId({ columns: COLUMNS, currentColumnId: 'missing', step: 1, isEditable: isColumnEditable });
assert.equal(next, null);
});
// rows — table.getRowModel().flatRows: включают итоговые строки (GROUP/ITEM/
// ROOT), которые isCellEditable делает нередактируемыми независимо от
// колонки (rowType !== 'INPUT'); здесь это просто row.editable === false.
const ROWS = [
{ id: 'row-1', editable: true },
{ id: 'row-2', editable: false }, // итоговая строка (GROUP)
{ id: 'row-3', editable: true },
{ id: 'row-4', editable: false }, // итоговая строка (ROOT)
];
const isRowEditable = (row) => row.editable;
test('Enter: moves down to the next row where the column is editable, skipping summary rows', () => {
const next = findNextEditableRowId({ rows: ROWS, currentRowId: 'row-1', step: 1, isEditable: isRowEditable });
assert.equal(next, 'row-3');
});
test('Enter at the last row returns null (конец таблицы)', () => {
const next = findNextEditableRowId({ rows: ROWS, currentRowId: 'row-3', step: 1, isEditable: isRowEditable });
assert.equal(next, null);
});
test('moving up (step: -1) finds the previous editable row', () => {
const prev = findNextEditableRowId({ rows: ROWS, currentRowId: 'row-3', step: -1, isEditable: isRowEditable });
assert.equal(prev, 'row-1');
});
test('an unknown current row id returns null instead of throwing', () => {
const next = findNextEditableRowId({ rows: ROWS, currentRowId: 'missing', step: 1, isEditable: isRowEditable });
assert.equal(next, null);
});