- 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>
160 lines
4.5 KiB
JavaScript
160 lines
4.5 KiB
JavaScript
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;
|