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

160 lines
4.5 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 { FormControl, MenuItem, Select } from '@mui/material';
import { memo, useCallback, useEffect, useLayoutEffect, useMemo, useRef, useState } from 'react';
import { useRealtimeActions, useVspOptions } from '../../contexts/RealtimeContext';
const EDITOR_STYLES = {
position: 'absolute',
zIndex: 12,
top: 0,
};
const VspDropdownEditCell = memo(({ refCell, onChange, disabled, value, table, cell }) => {
const { endEditing: contextEndEditing } = useRealtimeActions();
const vspOptions = useVspOptions();
const [selectedValue, setSelectedValue] = useState(() => value ?? '');
const [position, setPosition] = useState();
const [open, setOpen] = useState(false);
const refFinished = useRef(false);
// AURORA-1891: см. тот же приём в EditCell.jsx — фокус после закрытия
// редактора возвращается в <td>, чтобы клавиатурная навигация не отваливалась.
const tdRef = useRef(null);
useEffect(() => {
const timer = setTimeout(() => {
setOpen(true);
}, 80);
return () => clearTimeout(timer);
}, []);
useLayoutEffect(() => {
if (!refCell?.current) return;
const td = refCell.current.offsetParent;
const tr = td?.offsetParent;
if (td && tr) {
tdRef.current = td;
setPosition({
left: td.offsetLeft,
width: td.offsetWidth,
translateY: tr.style.transform || '',
});
}
}, [refCell]);
useEffect(() => {
return () => {
tdRef.current?.focus?.();
};
}, []);
const finishEditing = useCallback(() => {
if (refFinished.current) return;
refFinished.current = true;
table.setEditingCell(null);
const editingCell = table.getState().editingCell;
if (editingCell) {
contextEndEditing?.(editingCell.row, editingCell.column);
}
}, [table, contextEndEditing]);
// AURORA-1891: та же клавиатурная логика, что в EditCell.jsx — Esc уже
// работает через onClose MUI Select (reason !== 'selectOption' вызывает
// finishEditing без onChange, т.е. без cell_updated — отмена). Здесь
// добираем Tab/Shift+Tab и Enter: сохранить (для dropdown «сохранить» —
// это просто закрыть без выбора, onChange уже случился в handleSelectChange,
// если пользователь кликнул опцию) и перейти к следующей редактируемой ячейке.
const handleKeyDown = useCallback(
(event) => {
if (refFinished.current) return;
if (event.key === 'Tab') {
event.preventDefault();
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, event.shiftKey ? 'left' : 'right');
finishEditing();
return;
}
if (event.key === 'Enter') {
event.preventDefault();
table.options.meta?.openNextEditableCell?.(cell.row, cell.column, 'down');
finishEditing();
}
},
[cell, finishEditing, table],
);
const handleSelectChange = useCallback(
(e) => {
const val = e.target.value;
if (val == null || val === '') return;
refFinished.current = true;
setSelectedValue(val);
onChange?.(val);
const editingCell = table.getState().editingCell;
if (editingCell) {
contextEndEditing?.(editingCell.row, editingCell.column);
}
},
[onChange, table, contextEndEditing],
);
const handleSelectClose = useCallback(
(_event, reason) => {
if (reason === 'selectOption') return;
finishEditing();
},
[finishEditing],
);
const editorStyles = useMemo(
() => ({
...EDITOR_STYLES,
left: position?.left || 0,
width: position?.width || 0,
transform: position?.translateY || 0,
}),
[position],
);
return (
<tr>
<td>
{position && (
<div style={editorStyles}>
<FormControl fullWidth size='small'>
<Select
value={selectedValue}
onChange={handleSelectChange}
onKeyDown={handleKeyDown}
open={open}
onClose={handleSelectClose}
displayEmpty
disabled={disabled}
MenuProps={{
anchorOrigin: { vertical: 'bottom', horizontal: 'left' },
transformOrigin: { vertical: 'top', horizontal: 'left' },
}}>
<MenuItem value='' disabled>
Выберите ВСП
</MenuItem>
{vspOptions.map((option) => (
<MenuItem key={option.id} value={option.id}>
{option.registration_number}
</MenuItem>
))}
</Select>
</FormControl>
</div>
)}
</td>
</tr>
);
});
VspDropdownEditCell.displayName = 'VspDropdownEditCell';
export default VspDropdownEditCell;