AURORA-1891: редактирование одним кликом и с клавиатуры, без лишних записей в аудит #290

Open
aep wants to merge 6 commits from feat/284-aurora-1891-cell-click-keyboard-edit into test
Owner

Closes #284.

Стек веток (важно для ревью)

Эта ветка стоит не на test, а поверх четырёх незамерженных PR:

test → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным + nonNegativeRule, merge-коммит d518961) → PR #281 (AURORA-1890, переписанная редактируемая/нередактируемая/закрытая стилистика Cell.jsx через isCellEditable/isComputedColumn) → PR #283 (AURORA-1893, переписанный пайплайн ошибок валидации и ValidationErrorsAlert) → эта ветка.

Диф этого PR временно включает коммиты #277, #278, #281 и #283 (они ещё не в test) — как и каждый из них сам документирует это для своих предшественников. PR создан с --base test по процессу; после мерджа предыдущих четырёх ревьюеру будет проще смотреть только изменения этой задачи (последний коммит, feat(table): редактирование одним кликом и с клавиатуры...).

⚠️ Реальный конфликт с текущим test (не просто стек) — нужно внимание при мердже

В отличие от #277/#278/#281/#283, у этой ветки есть настоящий текстовый конфликт с нынешним концом test, а не только разница из-за стека: test ушёл вперёд прямым коммитом 21aded2 («perf(table): статичные стили ячеек в CSS контейнера, overscan строк 2 и колонок 3», 2026-10-28), который убрал динамическую getTableBodyCellProps/sx из constants/tableConfig.js и переписал muiTableBodyCellProps на ({ column }) => ({ 'data-col-id': column.id }) + статические CSS-классы (utils/activeCellCss.js, getActiveCellSx). Мой коммит добавляет в тот же muiTableBodyCellProps новые onClick/onKeyDown (и новый импорт useLockedCellsStore в соседней строке) — отсюда конфликт в RealtimeTable.jsx на обоих местах.

Проверено: ветка d7ab4d1 (всё до этой задачи, т.е. весь стек #277→#278→#281→#283) мержится в origin/test без конфликтов (git merge --no-commit --no-ff origin/test прошёл чисто). Конфликт появляется именно с коммитом AURORA-1891 этого PR. Специально не резолвил его здесь (--no-ff merge-попытка сделана и abort'нута, рабочее дерево чистое) — совмещать новую логику открытия редактора с чужим CSS-рефактором тайм-кода ячеек требует решения, которое лучше принять при фактическом мердже стека (возможно, уже на другой базе, если #277/#278/#281/#283 к тому моменту сами смержены и test снова изменится), а не дублировать работу сейчас. Если мерджить будет человек — ему нужно руками перенести onClick/onKeyDown из моей версии muiTableBodyCellProps в CSS-классовую версию из 21aded2.

Проблема

Пользователю непонятно, куда можно вводить данные, а где формулы/закрытые поля; переход в ячейку требовал ��вух кликов (двойной клик включал и тут же выключал выделение строки), а двойной клик по нередактируемой ячейке всё равно открывал (disabled) редактор и ставил серверную блокировку — коллеги видели 🔒 на ячейке, в которую нельзя даже писать.

Что сделано

  • utils/cellEditability.js — без изменений, только переиспользован isCellEditable из PR #281.
  • tableColumns.jsx (processColumns) — col.enableEditing теперь выставляется через isCellEditable для каждой листовой колонки (раньше — только для editType === 'vsp_dropdown', остальные листовые колонки MRT считал редактируемыми всегда, включая вычисляемые поля и колонки вне текущего этапа). isEditable(row, column) внутри файла изменена на isEditable(row, columnId) — голый id, чтобы один вызов годился и для настоящих MRT-колонок, и для листовых колонок конфига (col.accessorKey), у которых своего column.id ещё нет.
  • RealtimeTable.jsx:
    • onEditingCellChange — ранний выход, если ячейка не редактируема (isColumnActuallyEditable), защита на случай обхода нашего собственного onClick/onKeyDown.
    • muiTableBodyCellProps.onClick — на редактируемой, не заблокированной (lockedCellsStore.hasCell) ячейке, когда ничего не редактируется, stopPropagation() + table.setEditingCell(cell); иначе клик всплывает на muiTableBodyRowProps.onClick и выделяет строку, как раньше.
    • muiTableBodyCellProps.onKeyDown — на выделенной (сфокусированной) ячейке: Enter/F2 открывает редактор; Delete/Backspace — с пустым значением; печатный символ (key.length === 1, без Ctrl/Meta/Alt) — открывает редактор и подставляет этот символ вместо текущего значения. «Начальный ввод» хранится в state (editorInitialInput) и отдаётся в EditCell через table.options.meta.getEditorInitialInput() — тем же приёмом, что уже используется для getSelectedColumnId/getColumnTitle (стабильная обёртка в meta, актуальное значение — в ref).
    • openNextEditableCell(row, column, direction) (в meta) — резолвит реальные MRT row/column и зовёт чистые findNextEditableColumnId/findNextEditableRowId (utils/cellNavigation.js) по getOrderedVisibleColumns(table) и table.getRowModel().flatRows (не .rows — та же причина, что в рулинге PR #283: .rows отфильтрован состоянием свёрнутости групп через expandRows()/paginateExpandedRows, строка в свёрнутой группе там не нашлась бы). Прокручивает к цели (handleNavigateToColumn + rowVirtualizerRef.scrollToIndex, по образцу navigateToSearchMatch), затем помечает цель в pendingEditTargetRef — реально открывается она позже.
    • Эффект, слушающий dataEditingCells (WS cell_edit_start/cell_edit_end для своей сессии), дополнен: когда editingCell сбрасывается в null (после cell_edit_end текущей ячейки) и есть отложенная цель в pendingEditTargetRef, тем же путём (table.setEditingCell → onEditingCellChange → contextStartEditing → WS cell_edit_start) открывается следующая. Порядок WS-событий: cell_updated → cell_edit_end → cell_edit_start следующей — как того требует тикет.
  • Cell/Cell/Cell.jsx — удалён мёртвый onClick/handleClick (tableColumns.jsx никогда не передавал onClick в Cell).
  • Cell/EditCell/EditCell.jsx:
    • handleKeyDown: Escape — отмена, handleSave не вызывается, только завершение редактирования текущей ячейки (endCurrentEditing, вынесена из finishEditing); Tab/Shift+Tab — вызывает meta.openNextEditableCell(..., 'right'|'left') и сохраняет; Enter (без Shift) — meta.openNextEditableCell(..., 'down') и сохраняет; Shift+Enter — как раньше, перенос строки в textarea.
    • handleSave — если новое значение совпадает со старым (isSameCellValue, не формула) — onChange не вызывается, редактирование просто завершается без cell_updated.
    • Фокус: setTimeout(50) → requestAnimationFrame; на размонтирование редактора фокус возвращается в <td> этой ячейки (tdRef, захватывается в том же эффекте, что считает позицию), чтобы стрелки/Tab продолжали работать сразу после первого редактирования.
    • Начальное значение — useState-инициализатор проверяет table.options.meta.getEditorInitialInput() один раз при монтировании.
  • Cell/EditCell/VspDropdownEditCell.jsx — та же клавиатурная логика: Esc уже работал через onClose MUI Select (reason !== 'selectOption' → finishEditing() без onChange); добавлены Tab/Shift+Tab/Enter (вызывают meta.openNextEditableCell) и тот же приём возврата фокуса в <td> на размонтирование. Добавлен проп cell (уже передавался в editCellProps из EditCellPortal, но не был в сигнатуре компонента).
  • utils/cellNavigation.js (новый) — findNextEditableColumnId/findNextEditableRowId: чистые функции обхода уже готового списка (видимые листовые колонки / flatRows) с инъекцией предиката isEditable; ничего не знают про MRT/isCellEditable.
  • utils/formulaUtils.js — isSameCellValue(a, b): число vs строка, нормализация тысячных пробелов и десятичной запятой ("1 500,0" ~ 1500), пусто vs null/undefined; формула никогда не считается неизменной (даже текстуально идентичная — чтобы handleSave по-прежнему всегда её пересчитывал).

Тесты

Новые (TDD, писались до реализации):

  • node --test src/components/RealtimeTable/utils/cellNavigation.test.js — 9 тестов: следующая/предыдущая редактируемая колонка с пропуском нередактируемых (скрытые колонки просто не передаются в массиве — так же, как getOrderedVisibleColumns их не возвращает), следующая/предыдущая строка с пропуском итоговых строк, конец строки/таблицы → null, неизвестный текущий id → null.
  • node --test src/components/RealtimeTable/utils/formulaUtils.test.js — 6 тестов: "1500" vs 1500, "1 500,0" vs 1500, ""/null/undefined взаимно равны, формула никогда не «неизменна» (даже против идентичного текста формулы), разные значения не равны, текст сравнивается после trim.

Полный прогон node --test src/components/RealtimeTable/utils/*.test.js — до изменений: 70 тестов, 69 passed, 1 fail (columnLayoutUtils.test.js); после: 85 тестов (+15 новых), 84 passed, тот же единственный pre-existing fail (columnLayoutUtils.test.js, падает из-за отсутствующего node_modules/@tanstack/table-core в этом worktree-окружении, не связан с изменениями этого PR — та же причина уже отмечена в описаниях #278/#281/#283).

Ручная проверка — по коду (живой стенд не поднимал, как и предыдущие PR в этой цепочке): один клик по редактируемой ячейке открывает редактор с фокусом на textarea; клик по вычисляемой/закрытой ячейке не открывает редактор и выделяет строку, блокировка не ставится (нет вызова contextStartEditing, значит нет WS cell_edit_start, значит 🔒 у коллег не появляется); Esc не вызывает onChange/updateCell; Tab/Enter вызывают openNextEditableCell, которая прокручивает виртуализатор и контейнер колонок так же, как при переходе к ошибке/результату поиска; печатный символ на выделенной ячейке передаётся в EditCell как начальное значение; колонка ВСП получила тот же набор клавиш.

Критерий приёмки

  • Редактирование начинается одним кликом или с клавиатуры на выделенной ячейке — да.
  • Нередактируемые ячейки редактор не открывают и для других не блокируются — да (enableEditing на MRT-колонке + защитная проверка в onEditingCellChange).
  • Esc отменяет ввод; Tab и Enter сохраняют и переводят к следующей доступной ячейке — да.
  • Открытие и закрытие ячейки без изменения не создаёт записи в журнале аудита — да (isSameCellValue пропускает onChange/cell_updated, когда значение не изменилось).

Пункт 8 тикета (backend websocket.py: не выдавать блокировку, если v3.can_edit вернёт ok=false) сознательно не делался — тикет сам выносит его в отдельный PR.

Rulings (решения без отдельного обсуждения)

  • isColumnActuallyEditable вместо голого cellIsEditable: номер строки (accessorKey: 'sort_order') имеет жёсткий enableEditing: false в tableColumns.jsx, но независимая проверка через isCellEditable для INPUT-строки у администратора вернула бы true (админ редактирует что угодно на INPUT-строке, если это не вычисляемое поле — а sort_order не начинается с data., значит не вычисляемое). Без дополнительной сверки с реальным column.columnDef.enableEditing клик/Tab у админа могли бы попытаться открыть несуществующий редактор для колонки номера строки вместо выделения строки/перехода дальше. На практике это уже прикрыто stopPropagation() в самой кнопке номера строки, но полагаться только на это было бы хрупко.
  • «Начальный ввод» передаётся через table.options.meta.getEditorInitialInput(), а не отдельным явным пропом в цепочке tableColumns.jsx → EditCellPortal → EditCell: тикет говорит «передать пропом в EditCell», но добавление нового поля в getCachedEditProps/editCellProps требовало бы расширять кэш-инвалидацию в tableColumns.jsx на каждое нажатие клавиши. Выбран тот же приём, что уже использует этот файл для getSelectedColumnId/getColumnTitle — стабильная функция в meta, актуальное значение берётся из ref. EditCell всё равно получает значение через свой table-проп, технически это «проп», просто не отдельный одноимённый.
  • Переход по клику на другую ячейку во время редактирования текущей не реализован как «мгновенный прыжок» одним кликом: существующий mousedown-слушатель в EditCell.jsx (click-outside-to-close) закрывает текущую ячейку, но editingCell в React-state обновляется только после WS round-trip, поэтому click-обработчик новой ячейки в этот же тик видит старое значение editingCell и не открывает её — клик просто выделяет строку, открыть новую ячейку нужно вторым кликом. Это не регрессия (раньше для перехода между ячейками мышью требовался двойной клик на новую ячейку в любом случае) и тикет не требует иного для мыши — только для Tab/Enter, которые обработаны как положено.
  • Space (' ') как «начальный ввод»: тикет формулирует условие буквально как event.key.length === 1 без исключений — пробел подпадает под него, поэтому пробел на выделенной ячейке тоже открывает редактор и подставляет пробел.
  • isSameCellValue и колонка ВСП: сравнение «было/стало» добавлено только в EditCell.jsx (п.5 тикета называет только его handleSave). VspDropdownEditCell.handleSelectChange не трогался — повторный выбор уже выбранного значения в MUI Select всё равно вызовет onChange/cell_updated (edge-case вне формулировки тикета, пункта про ВСП в п.5 не было).
Closes #284. ## Стек веток (важно для ревью) Эта ветка стоит не на `test`, а поверх четырёх незамерженных PR: `test` → PR #277 (AURORA-1894, фикс красной рамки ошибки/тоста) → PR #278 (AURORA-1892, отрицательные суммы красным + nonNegativeRule, merge-коммит `d518961`) → PR #281 (AURORA-1890, переписанная редактируемая/нередактируемая/закрытая стилистика Cell.jsx через `isCellEditable`/`isComputedColumn`) → PR #283 (AURORA-1893, переписанный пайплайн ошибок валидации и `ValidationErrorsAlert`) → эта ветка. Диф этого PR временно включает коммиты #277, #278, #281 и #283 (они ещё не в `test`) — как и каждый из них сам документирует это для своих предшественников. PR создан с `--base test` по процессу; после мерджа предыдущих четырёх ревьюеру будет проще смотреть только изменения этой задачи (последний коммит, `feat(table): редактирование одним кликом и с клавиатуры...`). ### ⚠️ Реальный конфликт с текущим test (не просто стек) — нужно внимание при мердже В отличие от #277/#278/#281/#283, у этой ветки есть **настоящий текстовый конфликт** с нынешним концом `test`, а не только разница из-за стека: `test` ушёл вперёд прямым коммитом `21aded2` («perf(table): статичные стили ячеек в CSS контейнера, overscan строк 2 и колонок 3», 2026-10-28), который убрал динамическую `getTableBodyCellProps`/`sx` из `constants/tableConfig.js` и переписал `muiTableBodyCellProps` на `({ column }) => ({ 'data-col-id': column.id })` + статические CSS-классы (`utils/activeCellCss.js`, `getActiveCellSx`). Мой коммит добавляет в тот же `muiTableBodyCellProps` новые `onClick`/`onKeyDown` (и новый импорт `useLockedCellsStore` в соседней строке) — отсюда конфликт в `RealtimeTable.jsx` на обоих местах. Проверено: ветка `d7ab4d1` (всё до этой задачи, т.е. весь стек #277→#278→#281→#283) мержится в `origin/test` **без конфликтов** (`git merge --no-commit --no-ff origin/test` прошёл чисто). Конфликт появляется именно с коммитом AURORA-1891 этого PR. Специально не резолвил его здесь (`--no-ff` merge-попытка сделана и abort'нута, рабочее дерево чистое) — совмещать новую логику открытия редактора с чужим CSS-рефактором тайм-кода ячеек требует решения, которое лучше принять при фактическом мердже стека (возможно, уже на другой базе, если #277/#278/#281/#283 к тому моменту сами смержены и `test` снова изменится), а не дублировать работу сейчас. Если мерджить будет человек — ему нужно руками перенести `onClick`/`onKeyDown` из моей версии `muiTableBodyCellProps` в CSS-классовую версию из `21aded2`. ## Проблема Пользователю непонятно, куда можно вводить данные, а где формулы/закрытые поля; переход в ячейку требовал ��вух кликов (двойной клик включал и тут же выключал выделение строки), а двойной клик по нередактируемой ячейке всё равно открывал (disabled) редактор и ставил серверную блокировку — коллеги видели 🔒 на ячейке, в которую нельзя даже писать. ## Что сделано - **`utils/cellEditability.js`** — без изменений, только переиспользован `isCellEditable` из PR #281. - **`tableColumns.jsx`** (`processColumns`) — `col.enableEditing` теперь выставляется через `isCellEditable` для **каждой листовой** колонки (раньше — только для `editType === 'vsp_dropdown'`, остальные листовые колонки MRT считал редактируемыми всегда, включая вычисляемые поля и колонки вне текущего этапа). `isEditable(row, column)` внутри файла изменена на `isEditable(row, columnId)` — голый id, чтобы один вызов годился и для настоящих MRT-колонок, и для листовых колонок конфига (`col.accessorKey`), у которых своего `column.id` ещё нет. - **`RealtimeTable.jsx`**: - `onEditingCellChange` — ранний выход, если ячейка не редактируема (`isColumnActuallyEditable`), защита на случай обхода нашего собственного `onClick`/`onKeyDown`. - `muiTableBodyCellProps.onClick` — на редактируемой, не заблокированной (`lockedCellsStore.hasCell`) ячейке, когда ничего не редактируется, `stopPropagation()` + `table.setEditingCell(cell)`; иначе клик всплывает на `muiTableBodyRowProps.onClick` и выделяет строку, как раньше. - `muiTableBodyCellProps.onKeyDown` — на выделенной (сфокусированной) ячейке: `Enter`/`F2` открывает редактор; `Delete`/`Backspace` — с пустым значением; печатный символ (`key.length === 1`, без Ctrl/Meta/Alt) — открывает редактор и подставляет этот символ вместо текущего значения. «Начальный ввод» хранится в `state` (`editorInitialInput`) и отдаётся в `EditCell` через `table.options.meta.getEditorInitialInput()` — тем же приёмом, что уже используется для `getSelectedColumnId`/`getColumnTitle` (стабильная обёртка в `meta`, актуальное значение — в ref). - `openNextEditableCell(row, column, direction)` (в `meta`) — резолвит реальные MRT row/column и зовёт чистые `findNextEditableColumnId`/`findNextEditableRowId` (`utils/cellNavigation.js`) по `getOrderedVisibleColumns(table)` и **`table.getRowModel().flatRows`** (не `.rows` — та же причина, что в рулинге PR #283: `.rows` отфильтрован состоянием свёрнутости групп через `expandRows()`/`paginateExpandedRows`, строка в свёрнутой группе там не нашлась бы). Прокручивает к цели (`handleNavigateToColumn` + `rowVirtualizerRef.scrollToIndex`, по образцу `navigateToSearchMatch`), затем помечает цель в `pendingEditTargetRef` — реально открывается она позже. - Эффект, слушающий `dataEditingCells` (WS `cell_edit_start`/`cell_edit_end` для своей сессии), дополнен: когда `editingCell` сбрасывается в `null` (после `cell_edit_end` текущей ячейки) и есть отложенная цель в `pendingEditTargetRef`, тем же путём (`table.setEditingCell` → `onEditingCellChange` → `contextStartEditing` → WS `cell_edit_start`) открывается следующая. Порядок WS-событий: `cell_updated` → `cell_edit_end` → `cell_edit_start` следующей — как того требует тикет. - **`Cell/Cell/Cell.jsx`** — удалён мёртвый `onClick`/`handleClick` (tableColumns.jsx никогда не передавал `onClick` в `Cell`). - **`Cell/EditCell/EditCell.jsx`**: - `handleKeyDown`: `Escape` — отмена, `handleSave` не вызывается, только завершение редактирования текущей ячейки (`endCurrentEditing`, вынесена из `finishEditing`); `Tab`/`Shift+Tab` — вызывает `meta.openNextEditableCell(..., 'right'|'left')` и сохраняет; `Enter` (без Shift) — `meta.openNextEditableCell(..., 'down')` и сохраняет; `Shift+Enter` — как раньше, перенос строки в textarea. - `handleSave` — если новое значение совпадает со старым (`isSameCellValue`, не формула) — `onChange` не вызывается, редактирование просто завершается без `cell_updated`. - Фокус: `setTimeout(50)` → `requestAnimationFrame`; на размонтирование редактора фокус возвращается в `<td>` этой ячейки (`tdRef`, захватывается в том же эффекте, что считает позицию), чтобы стрелки/Tab продолжали работать сразу после первого редактирования. - Начальное значение — `useState`-инициализатор проверяет `table.options.meta.getEditorInitialInput()` один раз при монтировании. - **`Cell/EditCell/VspDropdownEditCell.jsx`** — та же клавиатурная логика: `Esc` уже работал через `onClose` MUI `Select` (`reason !== 'selectOption'` → `finishEditing()` без `onChange`); добавлены `Tab`/`Shift+Tab`/`Enter` (вызывают `meta.openNextEditableCell`) и тот же приём возврата фокуса в `<td>` на размонтирование. Добавлен проп `cell` (уже передавался в `editCellProps` из `EditCellPortal`, но не был в сигнатуре компонента). - **`utils/cellNavigation.js`** (новый) — `findNextEditableColumnId`/`findNextEditableRowId`: чистые функции обхода уже готового списка (видимые листовые колонки / `flatRows`) с инъекцией предиката `isEditable`; ничего не знают про MRT/`isCellEditable`. - **`utils/formulaUtils.js`** — `isSameCellValue(a, b)`: число vs строка, нормализация тысячных пробелов и десятичной запятой (`"1 500,0"` ~ `1500`), пусто vs `null`/`undefined`; формула никогда не считается неизменной (даже текстуально идентичная — чтобы `handleSave` по-прежнему всегда её пересчитывал). ## Тесты Новые (TDD, писались до реализации): - `node --test src/components/RealtimeTable/utils/cellNavigation.test.js` — 9 тестов: следующая/предыдущая редактируемая колонка с пропуском нередактируемых (скрытые колонки просто не передаются в массиве — так же, как `getOrderedVisibleColumns` их не возвращает), следующая/предыдущая строка с пропуском итоговых строк, конец строки/таблицы → `null`, неизвестный текущий id → `null`. - `node --test src/components/RealtimeTable/utils/formulaUtils.test.js` — 6 тестов: `"1500"` vs `1500`, `"1 500,0"` vs `1500`, `""`/`null`/`undefined` взаимно равны, формула никогда не «неизменна» (даже против идентичного текста формулы), разные значения не равны, текст сравнивается после trim. Полный прогон `node --test src/components/RealtimeTable/utils/*.test.js` — **до изменений**: 70 тестов, 69 passed, 1 fail (`columnLayoutUtils.test.js`); **после**: 85 тестов (+15 новых), 84 passed, тот же единственный pre-existing fail (`columnLayoutUtils.test.js`, падает из-за отсутствующего `node_modules/@tanstack/table-core` в этом worktree-окружении, не связан с изменениями этого PR — та же причина уже отмечена в описаниях #278/#281/#283). Ручная проверка — по коду (живой стенд не поднимал, как и предыдущие PR в этой цепочке): один клик по редактируемой ячейке открывает редактор с фокусом на textarea; клик по вычисляемой/закрытой ячейке не открывает редактор и выделяет строку, блокировка не ставится (нет вызова `contextStartEditing`, значит нет WS `cell_edit_start`, значит 🔒 у коллег не появляется); `Esc` не вызывает `onChange`/`updateCell`; `Tab`/`Enter` вызывают `openNextEditableCell`, которая прокручивает виртуализатор и контейнер колонок так же, как при переходе к ошибке/результату поиска; печатный символ на выделенной ячейке передаётся в `EditCell` как начальное значение; колонка ВСП получила тот же набор клавиш. ## Критерий приёмки - Редактирование начинается одним кликом или с клавиатуры на выделенной ячейке — да. - Нередактируемые ячейки редактор не открывают и для других не блокируются — да (`enableEditing` на MRT-колонке + защитная проверка в `onEditingCellChange`). - Esc отменяет ввод; Tab и Enter сохраняют и переводят к следующей доступной ячейке — да. - Открытие и закрытие ячейки без изменения не создаёт записи в журнале аудита — да (`isSameCellValue` пропускает `onChange`/`cell_updated`, когда значение не изменилось). Пункт 8 тикета (backend `websocket.py`: не выдавать блокировку, если `v3.can_edit` вернёт `ok=false`) сознательно не делался — тикет сам выносит его в отдельный PR. ## Rulings (решения без отдельного обсуждения) - **`isColumnActuallyEditable` вместо голого `cellIsEditable`**: номер строки (`accessorKey: 'sort_order'`) имеет жёсткий `enableEditing: false` в `tableColumns.jsx`, но независимая проверка через `isCellEditable` для INPUT-строки у администратора вернула бы `true` (админ редактирует что угодно на INPUT-строке, если это не вычисляемое поле — а `sort_order` не начинается с `data.`, значит не вычисляемое). Без дополнительной сверки с реальным `column.columnDef.enableEditing` клик/Tab у админа могли бы попытаться открыть несуществующий редактор для колонки номера строки вместо выделения строки/перехода дальше. На практике это уже прикрыто `stopPropagation()` в самой кнопке номера строки, но полагаться только на это было бы хрупко. - **«Начальный ввод» передаётся через `table.options.meta.getEditorInitialInput()`, а не отдельным явным пропом в цепочке `tableColumns.jsx` → `EditCellPortal` → `EditCell`**: тикет говорит «передать пропом в EditCell», но добавление нового поля в `getCachedEditProps`/`editCellProps` требовало бы расширять кэш-инвалидацию в `tableColumns.jsx` на каждое нажатие клавиши. Выбран тот же приём, что уже использует этот файл для `getSelectedColumnId`/`getColumnTitle` — стабильная функция в `meta`, актуальное значение берётся из ref. `EditCell` всё равно получает значение через свой `table`-проп, технически это «проп», просто не отдельный одноимённый. - **Переход по клику на другую ячейку во время редактирования текущей** не реализован как «мгновенный прыжок» одним кликом: существующий `mousedown`-слушатель в `EditCell.jsx` (click-outside-to-close) закрывает текущую ячейку, но `editingCell` в React-state обновляется только после WS round-trip, поэтому click-обработчик новой ячейки в этот же тик видит старое значение `editingCell` и не открывает её — клик просто выделяет строку, открыть новую ячейку нужно вторым кликом. Это не регрессия (раньше для перехода между ячейками мышью требовался двойной клик на новую ячейку в любом случае) и тикет не требует иного для мыши — только для Tab/Enter, которые обработаны как положено. - **Space (`' '`) как «начальный ввод»**: тикет формулирует условие буквально как `event.key.length === 1` без исключений — пробел подпадает под него, поэтому пробел на выделенной ячейке тоже открывает редактор и подставляет пробел. - **`isSameCellValue` и колонка ВСП**: сравнение «было/стало» добавлено только в `EditCell.jsx` (п.5 тикета называет только его `handleSave`). `VspDropdownEditCell.handleSelectChange` не трогался — повторный выбор уже выбранного значения в MUI `Select` всё равно вызовет `onChange`/`cell_updated` (edge-case вне формулировки тикета, пункта про ВСП в п.5 не было).
aep added 6 commits 2026-11-01 15:15:38 +03:00
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>
Cell.jsx: formatNumber вынесен в utils/numberFormat.js и возвращает
{ text, isNegative }; отрицательные числа выделяются цветом (#C62828 на
светлом фоне, #FFCDD2 на тёмном) и font-weight 600, ноль остаётся «-».

constants/validation.js: фабрика nonNegativeRule и общий список
nonNegativeColumnKeys (план, бронь на 2026/2027, факт по месяцам и СПОД,
сумма платежа, предельная стоимость договора/ЦКК, сателлиты АХР
Аренда/Коммунальные/Безопасность, АХР Лимиты целиком). Корректировки и
переносы (adj_*, base_correction, revision_*, seq_*, target_change,
transfer_*) не включены — там минус — нормальная операция. Запрет
мягкий: подсветка и запись в «Контроль ошибок» через существующий
useValidationRules/isCellInvalid, сохранение не блокируется.

EditCell.jsx: handleSave предупреждает toast.warning при сохранении
отрицательного числа в защищённой колонке, значение всё равно
сохраняется. useValidationRules и RealtimeTable прокидывают
человекочитаемый заголовок колонки для текста предупреждения.

Тесты (node --test): numberFormat.test.js, validationUtils.test.js —
новые, 10/10 зелёных.
# Conflicts:
#	web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
- utils/cellEditability.js: isCellEditable/isComputedColumn — чистые функции,
  isComputedColumn повторяет часть v3._apply_form_cell/_apply_form3_cell
  (0023_apply_cell_empty_string.sql), чтобы админ не видел вычисляемые
  колонки как редактируемые. tableColumns.jsx использует isCellEditable
  вместо инлайновой проверки.
- useRealtimeData.js: возвращает stageColumnDeadlines (closes_at по колонке
  текущего этапа) рядом с columnsCurStage.
- Cell.jsx: редактируемая ячейка — белый фон, рамка 1px #7FA7C9 (2px при
  наведении), курсор text; нередактируемая ячейка строки INPUT — фон
  #F4F5F7, текст #5F6B7A; занятая коллегой — пунктирная оранжевая рамка
  2px #F0A030 без изменения фона. Строки ITEM/GROUP/ROOT не затронуты.
  Сохранён порядок стилей из AURORA-1894 (INVALID_STYLES последним) и
  спред fontWeight для отрицательных чисел из AURORA-1892.
- TableHead.jsx: метка срока ввода «✎ до DD.MM HH:mm» в заголовке колонки
  текущего этапа, только для исполнителя (utils/deadlineFormat.js).
- SettingPanel/CellLegend.jsx: иконка "?" с легендой (можно вводить /
  расчёт или закрыто) рядом с плашкой контроля ошибок, без новой панели
  и без новых цветов в palettes columnColors.js.

node --test: cellEditability.test.js, deadlineFormat.test.js — 18/18.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- validationUtils.collectInvalidErrors теперь собирает ошибки по ЯЧЕЙКАМ
  (id/ruleId/columnId/lineId/rowLabel/message), а не одну запись на колонку;
  новая groupValidationErrors(errors) группирует их обратно по правилу+колонке
  со счётчиком для меню плашки.
- useValidationRules: getCellErrors(columnId, value, row) отдаёт причины
  конкретной ячейки (для подсказки), isCellInvalid выражен через него;
  сравнение стабильности errors — по id (он уже кодирует правило+колонку+строку).
- RealtimeTable: handleNavigateToError({lineId, columnId}) — находит строку по
  getRowId в table.getRowModel().flatRows (не .rows, т.к. flatRows не фильтруется
  по свёрнутым группам), раскрывает родителей через setExpanded, включает
  скрытую колонку через handleSetColumnsVisibility, выделяет ячейку как
  activeSearchMatch и скроллит к ней — по образцу navigateToSearchMatch.
- ValidationErrorsAlert: красная плашка (#FDEDED/#D32F2F) со счётчиком
  «Ошибок: N», кнопками ‹›  для прохода по всем ошибкам и меню, сгруппированным
  по правилу+колонке (до 20 строк, дальше «и ещё N»); перенесена в начало
  SettingPanel, перед группами настроек.
- tableColumns/Cell: invalidMessage (из getCellErrors) доходит до Cell и
  показывается подсказкой (MUI Tooltip) при наведении на невалидную ячейку.

Тесты (node --test .../validationUtils.test.js): 12/12, включая 5 существующих
тестов nonNegativeRule из AURORA-1892 (не менялись, подтверждают отсутствие
регрессии) и 7 новых для collectInvalidErrors/groupValidationErrors.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
- 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>
This pull request has changes conflicting with the target branch.
  • web/src/components/RealtimeTable/RealtimeTable.jsx

Checkout

From your project repository, check out a new branch and test the changes.
git fetch -u origin feat/284-aurora-1891-cell-click-keyboard-edit:feat/284-aurora-1891-cell-click-keyboard-edit
git checkout feat/284-aurora-1891-cell-click-keyboard-edit
Sign in to join this conversation.
No Reviewers
1 Participants
Notifications
Due Date
No due date set.
Dependencies

No dependencies set.

Reference: RSHB/DFiP_Budget_planing#290
No description provided.