ws - в процессе
This commit is contained in:
parent
8b16901f1b
commit
079e719f71
@ -1,210 +1,91 @@
|
||||
import React, { useState, useEffect, useRef, useCallback } from 'react';
|
||||
import { Box, Tooltip, CircularProgress } from '@mui/material';
|
||||
import React from 'react';
|
||||
import { useMemo } from 'react';
|
||||
import styled from '@emotion/styled';
|
||||
import { useRealtime } from '../../contexts/RealtimeContext';
|
||||
|
||||
const Cell = ({ value, row, column, onSave, userId, isEditable = true }) => {
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [editValue, setEditValue] = useState(value);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [lockInfo, setLockInfo] = useState(null);
|
||||
|
||||
const {
|
||||
isCellLocked,
|
||||
getCellLockInfo,
|
||||
updateCell,
|
||||
subscribeToErrors,
|
||||
tryLockCell,
|
||||
unlockCell
|
||||
} = useRealtime();
|
||||
|
||||
const inputRef = useRef(null);
|
||||
const rowId = row.original?.id || row.id || row.index;
|
||||
const columnId = column.id;
|
||||
|
||||
// Проверяем блокировку ячейки
|
||||
useEffect(() => {
|
||||
if (!isEditing) {
|
||||
const checkLock = () => {
|
||||
const lock = getCellLockInfo(rowId, columnId);
|
||||
setLockInfo(lock);
|
||||
};
|
||||
|
||||
checkLock();
|
||||
const interval = setInterval(checkLock, 2000); // Проверяем каждые 2 секунды
|
||||
return () => clearInterval(interval);
|
||||
}
|
||||
}, [rowId, columnId, isEditing, getCellLockInfo]);
|
||||
const ContainerForText = styled.span`
|
||||
display: -webkit-box;
|
||||
-webkit-box-orient: vertical;
|
||||
-webkit-line-clamp: 3;
|
||||
overflow: hidden;
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
// Подписка на ошибки
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeToErrors((error) => {
|
||||
console.error('Cell update error:', error);
|
||||
setIsSaving(false);
|
||||
setIsEditing(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [subscribeToErrors]);
|
||||
// Функция для подсветки текста
|
||||
const highlightText = (text, searchQuery) => {
|
||||
if (!searchQuery || !text) return text;
|
||||
|
||||
const isLocked = isCellLocked(rowId, columnId);
|
||||
const isLockedByMe = lockInfo?.userId === userId;
|
||||
// Экранируем спецсимволы в поисковом запросе
|
||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||
|
||||
const handleDoubleClick = useCallback(() => {
|
||||
if (!isEditable) return;
|
||||
|
||||
if (!isLocked || isLockedByMe) {
|
||||
// Пытаемся заблокировать ячейку
|
||||
if (tryLockCell(rowId, columnId, userId)) {
|
||||
setIsEditing(true);
|
||||
setEditValue(value);
|
||||
setTimeout(() => inputRef.current?.focus(), 100);
|
||||
} else {
|
||||
const lock = getCellLockInfo(rowId, columnId);
|
||||
console.warn(`Cell is locked by user ${lock?.userId}`);
|
||||
}
|
||||
}
|
||||
}, [isEditable, isLocked, isLockedByMe, tryLockCell, rowId, columnId, userId, value, getCellLockInfo]);
|
||||
const parts = text.split(regex);
|
||||
|
||||
const handleSave = useCallback(async () => {
|
||||
if (editValue === value) {
|
||||
setIsEditing(false);
|
||||
unlockCell(rowId, columnId, userId);
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSaving(true);
|
||||
const success = await updateCell(rowId, columnId, editValue);
|
||||
|
||||
if (success) {
|
||||
onSave?.(row, column, editValue);
|
||||
setIsEditing(false);
|
||||
} else {
|
||||
setIsSaving(false);
|
||||
}
|
||||
}, [editValue, value, updateCell, rowId, columnId, userId, onSave, row, column, unlockCell]);
|
||||
|
||||
const handleKeyDown = useCallback((e) => {
|
||||
if (e.key === 'Enter') {
|
||||
handleSave();
|
||||
} else if (e.key === 'Escape') {
|
||||
setIsEditing(false);
|
||||
setEditValue(value);
|
||||
unlockCell(rowId, columnId, userId);
|
||||
}
|
||||
}, [handleSave, value, rowId, columnId, userId, unlockCell]);
|
||||
|
||||
// Отмена блокировки при размонтировании, если ячейка редактировалась
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
if (isEditing) {
|
||||
unlockCell(rowId, columnId, userId);
|
||||
}
|
||||
};
|
||||
}, [isEditing, rowId, columnId, userId, unlockCell]);
|
||||
|
||||
const getLockTooltip = () => {
|
||||
if (!isEditable) return 'This cell is not editable';
|
||||
if (isLocked && !isLockedByMe) {
|
||||
return `Editing by user ${lockInfo?.userId}`;
|
||||
}
|
||||
if (isLockedByMe && isEditing) {
|
||||
return 'You are editing this cell';
|
||||
}
|
||||
return 'Double-click to edit';
|
||||
};
|
||||
|
||||
const getCellStyle = () => {
|
||||
const baseStyle = {
|
||||
padding: '8px',
|
||||
minHeight: '40px',
|
||||
position: 'relative',
|
||||
};
|
||||
|
||||
if (!isEditable) {
|
||||
return {
|
||||
...baseStyle,
|
||||
backgroundColor: '#fafafa',
|
||||
color: '#999',
|
||||
cursor: 'not-allowed',
|
||||
};
|
||||
}
|
||||
|
||||
if (isLocked && !isLockedByMe) {
|
||||
return {
|
||||
...baseStyle,
|
||||
backgroundColor: '#f5f5f5',
|
||||
opacity: 0.7,
|
||||
cursor: 'not-allowed',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
...baseStyle,
|
||||
cursor: 'text',
|
||||
'&:hover': {
|
||||
backgroundColor: '#f0f0f0',
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
if (isEditing) {
|
||||
return (
|
||||
<Box sx={{ position: 'relative', width: '100%' }}>
|
||||
<input
|
||||
ref={inputRef}
|
||||
type="text"
|
||||
value={editValue}
|
||||
onChange={(e) => setEditValue(e.target.value)}
|
||||
onBlur={handleSave}
|
||||
onKeyDown={handleKeyDown}
|
||||
disabled={isSaving}
|
||||
style={{
|
||||
width: '100%',
|
||||
padding: '8px',
|
||||
border: '2px solid #1976d2',
|
||||
borderRadius: '4px',
|
||||
fontSize: 'inherit',
|
||||
outline: 'none',
|
||||
}}
|
||||
/>
|
||||
{isSaving && (
|
||||
<CircularProgress
|
||||
size={20}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
right: 8,
|
||||
top: '50%',
|
||||
transform: 'translateY(-50%)',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Tooltip title={getLockTooltip()} placement="top" arrow>
|
||||
<Box
|
||||
onDoubleClick={handleDoubleClick}
|
||||
sx={getCellStyle()}
|
||||
return parts.map((part, index) =>
|
||||
regex.test(part) ? (
|
||||
<mark
|
||||
key={index}
|
||||
style={{
|
||||
backgroundColor: '#ffeb3b',
|
||||
color: '#000',
|
||||
fontWeight: 'bold',
|
||||
padding: '0 2px',
|
||||
borderRadius: '2px',
|
||||
}}
|
||||
>
|
||||
{value || ''}
|
||||
{isLockedByMe && !isEditing && (
|
||||
<Box
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: 2,
|
||||
right: 2,
|
||||
width: 8,
|
||||
height: 8,
|
||||
borderRadius: '50%',
|
||||
backgroundColor: '#ff9800',
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
</Tooltip>
|
||||
{part}
|
||||
</mark>
|
||||
) : (
|
||||
part
|
||||
),
|
||||
);
|
||||
};
|
||||
|
||||
export default Cell;
|
||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color }) => {
|
||||
const cellValue = cell.getValue();
|
||||
const textValue = String(cellValue || '');
|
||||
|
||||
// Подсвечиваем текст с учетом поискового запроса
|
||||
const highlightedContent = useMemo(() => {
|
||||
return highlightText(textValue, globalFilter);
|
||||
}, [textValue, globalFilter]);
|
||||
|
||||
const {
|
||||
isCellLocked,
|
||||
getCellLockInfo,
|
||||
updateCell,
|
||||
subscribeToErrors,
|
||||
tryLockCell,
|
||||
unlockCell
|
||||
} = useRealtime();
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className="cell"
|
||||
style={{
|
||||
width: '100%',
|
||||
height: '100%',
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
justifyContent: 'center',
|
||||
boxSizing: 'border-box',
|
||||
position: 'absolute',
|
||||
top: 0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
bottom: 0,
|
||||
border: '2px solid transparent',
|
||||
padding: '8px',
|
||||
backgroundColor: backgroundColor,
|
||||
color: color,
|
||||
}}
|
||||
onClick={onClick}
|
||||
>
|
||||
<ContainerForText>{highlightedContent}</ContainerForText>
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
export default Cell;
|
||||
|
||||
@ -1,10 +1,10 @@
|
||||
import React, { useState, useMemo, useRef, useEffect } from 'react';
|
||||
// components/RealtimeTable/index.js
|
||||
import React, { useState, useMemo, useRef, useEffect, useCallback } from 'react';
|
||||
import { createPortal } from 'react-dom';
|
||||
import {
|
||||
useMaterialReactTable,
|
||||
MaterialReactTable,
|
||||
} from 'material-react-table';
|
||||
import { CircularProgress, Typography, Box } from '@mui/material';
|
||||
|
||||
import useRealtimeData from './hooks/useRealtimeData';
|
||||
import { getTableColumns } from './tableColumns';
|
||||
@ -20,33 +20,28 @@ import {
|
||||
getTableHeadCellStyles,
|
||||
getTablePaperStyles,
|
||||
} from './constants/tableConfig';
|
||||
import { useRealtime } from './contexts/RealtimeContext';
|
||||
|
||||
// Импортируем WebSocket контекст
|
||||
import { RealtimeProvider, useRealtime } from './contexts/RealtimeContext';
|
||||
|
||||
// Внутренний компонент, который будет использовать WebSocket
|
||||
const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
// Получаем данные и состояние из хука
|
||||
const {
|
||||
data,
|
||||
setData,
|
||||
isConnected,
|
||||
isLoading: isDataLoading,
|
||||
error: wsError
|
||||
} = useRealtimeData(formId, sheetName);
|
||||
// isConnected = true
|
||||
|
||||
// Получаем функции из WebSocket контекста
|
||||
const { login, releaseAllLocks } = useRealtime();
|
||||
|
||||
const RealtimeTable = ({ formType, formId, sheetName }) => {
|
||||
const { data, setData } = useRealtimeData(formId, sheetName);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [globalFilter, setGlobalFilter] = useState('');
|
||||
const [showColumnFilters, setShowColumnFilters] = useState(false);
|
||||
const [selectedColumn, setSelectedColumn] = useState();
|
||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||
const [userId, setUserId] = useState(null);
|
||||
const [updatingCells, setUpdatingCells] = useState({});
|
||||
|
||||
const columnVirtualizerInstanceRef = useRef(null);
|
||||
|
||||
const {
|
||||
isConnected,
|
||||
isAuthenticated,
|
||||
subscribeToCellUpdates,
|
||||
subscribeToErrors,
|
||||
subscribeToAuthSuccess,
|
||||
error: wsError,
|
||||
updateCell: contextUpdateCell
|
||||
} = useRealtime();
|
||||
|
||||
const {
|
||||
columnSizing,
|
||||
columnPinning,
|
||||
@ -60,38 +55,19 @@ const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
} = useColumnSettings();
|
||||
|
||||
const { headerPortalRef, containerRef } = useHeaderPortal();
|
||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } = useTableScale(containerRef);
|
||||
|
||||
// Получаем ID пользователя
|
||||
useEffect(() => {
|
||||
const getUserId = async () => {
|
||||
try {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
// Декодируем JWT токен
|
||||
const payload = JSON.parse(atob(token.split('.')[1]));
|
||||
setUserId(payload.user_id || payload.sub || payload.id);
|
||||
} else {
|
||||
// Для разработки
|
||||
setUserId('anonymous_' + Date.now());
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Failed to get user ID:', error);
|
||||
setUserId('anonymous_' + Date.now());
|
||||
}
|
||||
};
|
||||
const { sizeMult, setSizeMult, tableScaleStyle, tableWrapperStyle } =
|
||||
useTableScale(containerRef);
|
||||
|
||||
getUserId();
|
||||
}, []);
|
||||
const [columnsConfig, setColumnsConfig] = useState(null);
|
||||
|
||||
// Загрузка конфигурации колонок
|
||||
useEffect(() => {
|
||||
const loadConfig = async () => {
|
||||
try {
|
||||
const { config } = await import(`./constants/${formType}/${sheetName}.js`);
|
||||
setColumnsConfig(config.config);
|
||||
} catch (error) {
|
||||
console.error(`Failed to load data for ${formType}/${sheetName}:`, error);
|
||||
console.error(`Failed to load config for ${formType}/${sheetName}:`, error);
|
||||
setColumnsConfig({ columns: [], colors: {} });
|
||||
}
|
||||
};
|
||||
@ -99,66 +75,108 @@ const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
loadConfig();
|
||||
}, [formType, sheetName]);
|
||||
|
||||
// Логинимся в WebSocket
|
||||
// Подписка на обновления от других пользователей
|
||||
useEffect(() => {
|
||||
if (isConnected && userId) {
|
||||
const token = localStorage.getItem('access_token');
|
||||
if (token) {
|
||||
login(token);
|
||||
const unsubscribe = subscribeToCellUpdates((updatedCells) => {
|
||||
console.log('Received updates from other users:', updatedCells);
|
||||
|
||||
setData(prevData => {
|
||||
if (!prevData) return prevData;
|
||||
const newData = [...prevData];
|
||||
updatedCells.forEach(update => {
|
||||
const rowIndex = newData.findIndex(row => row.id === update.line_id);
|
||||
if (rowIndex !== -1) {
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
[update.column]: update.value
|
||||
};
|
||||
}
|
||||
});
|
||||
return newData;
|
||||
});
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [subscribeToCellUpdates, setData]);
|
||||
|
||||
// Подписка на ошибки
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeToErrors((errorMsg) => {
|
||||
console.error('WebSocket error:', errorMsg);
|
||||
// Можно добавить Toast уведомление
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [subscribeToErrors]);
|
||||
|
||||
// Подписка на успешную авторизацию
|
||||
useEffect(() => {
|
||||
const unsubscribe = subscribeToAuthSuccess(() => {
|
||||
console.log('Successfully authenticated with WebSocket');
|
||||
setIsLoading(false);
|
||||
});
|
||||
|
||||
return unsubscribe;
|
||||
}, [subscribeToAuthSuccess]);
|
||||
|
||||
// Функция для установки состояния обновления ячейки
|
||||
const setUpdatingCell = useCallback((rowId, columnId, isUpdating) => {
|
||||
setUpdatingCells(prev => ({
|
||||
...prev,
|
||||
[`${rowId}_${columnId}`]: isUpdating
|
||||
}));
|
||||
}, []);
|
||||
|
||||
const handleUpdateCell = useCallback(async (rowId, columnId, value) => {
|
||||
setData(prevData => {
|
||||
const newData = [...prevData];
|
||||
const rowIndex = newData.findIndex(row => row.id === rowId);
|
||||
if (rowIndex !== -1) {
|
||||
newData[rowIndex] = {
|
||||
...newData[rowIndex],
|
||||
[columnId]: value
|
||||
};
|
||||
}
|
||||
}
|
||||
}, [isConnected, userId, login]);
|
||||
return newData;
|
||||
});
|
||||
|
||||
// Освобождаем блокировки при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
releaseAllLocks();
|
||||
};
|
||||
}, [releaseAllLocks]);
|
||||
// Отправляем через WebSocket
|
||||
return await contextUpdateCell(rowId, columnId, value);
|
||||
}, [contextUpdateCell, setData]);
|
||||
|
||||
// Обработчик сохранения ячейки (теперь только для оптимистичного обновления UI)
|
||||
const handleSaveCell = async (row, column, value) => {
|
||||
// Обновление данных происходит через WebSocket в Cell компоненте
|
||||
// Здесь просто обновляем UI для оптимистичного отображения
|
||||
const updatedData = [...data];
|
||||
const rowIndex = data.findIndex(r => (r.id || r.line_id || r.index) === row.id);
|
||||
if (rowIndex !== -1) {
|
||||
updatedData[rowIndex][column.id] = value;
|
||||
setData(updatedData);
|
||||
}
|
||||
};
|
||||
|
||||
// Создаем колонки с передачей userId в Cell
|
||||
// Создание колонок с интеграцией WebSocket
|
||||
const columns = useMemo(() => {
|
||||
if (!columnsConfig || !columnsConfig.columns) return [];
|
||||
|
||||
try {
|
||||
// Оборачиваем Cell компонент для передачи userId
|
||||
const CellWithProps = (props) => (
|
||||
<Cell
|
||||
{...props}
|
||||
userId={userId}
|
||||
onSave={handleSaveCell}
|
||||
/>
|
||||
);
|
||||
|
||||
return getTableColumns({
|
||||
Cell: CellWithProps,
|
||||
Cell,
|
||||
EditCell,
|
||||
columnsConfig
|
||||
columnsConfig,
|
||||
onCellUpdate: handleUpdateCell, // Передаем функцию обновления
|
||||
onCellUpdateError: (rowId, columnId, error) => {
|
||||
console.error(`Error updating cell ${rowId}_${columnId}:`, error);
|
||||
// Можно добавить Toast уведомление
|
||||
}
|
||||
});
|
||||
} catch (error) {
|
||||
console.error('Error creating columns:', error);
|
||||
return [];
|
||||
}
|
||||
}, [columnsConfig, userId, data]);
|
||||
}, [columnsConfig, handleUpdateCell]);
|
||||
|
||||
// Конфигурация таблицы (без изменений)
|
||||
// Метаданные для таблицы
|
||||
const tableMeta = useMemo(() => ({
|
||||
updatingCells,
|
||||
setUpdatingCell,
|
||||
updateCell: handleUpdateCell,
|
||||
}), [updatingCells, setUpdatingCell]);
|
||||
|
||||
// Конфигурация таблицы
|
||||
const tableConfig = useMemo(
|
||||
() => ({
|
||||
...BASE_TABLE_CONFIG,
|
||||
columns,
|
||||
data,
|
||||
data: data || [],
|
||||
columnVirtualizerInstanceRef,
|
||||
enableRowVirtualization: false,
|
||||
onColumnSizingChange: setColumnSizing,
|
||||
@ -171,6 +189,7 @@ const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
globalFilter,
|
||||
showColumnFilters,
|
||||
},
|
||||
meta: tableMeta,
|
||||
muiTableHeadCellProps: ({ column }) => ({
|
||||
sx: {
|
||||
width: column.getSize(),
|
||||
@ -224,41 +243,48 @@ const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
globalFilter,
|
||||
showColumnFilters,
|
||||
containerRef,
|
||||
tableMeta,
|
||||
],
|
||||
);
|
||||
|
||||
const table = useMaterialReactTable(tableConfig);
|
||||
|
||||
// Показываем индикатор загрузки
|
||||
if (isDataLoading) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Загрузка данных...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
// Индикатор подключения WebSocket
|
||||
const ConnectionStatus = () => (
|
||||
<div style={{
|
||||
position: 'fixed',
|
||||
bottom: 10,
|
||||
right: 10,
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
background: isConnected && isAuthenticated ? '#4caf50' : '#f44336',
|
||||
color: 'white',
|
||||
fontSize: '12px',
|
||||
zIndex: 1000,
|
||||
display: 'flex',
|
||||
alignItems: 'center',
|
||||
gap: '8px'
|
||||
}}>
|
||||
{console.log(isConnected ,isAuthenticated )}
|
||||
<span style={{
|
||||
display: 'inline-block',
|
||||
width: '8px',
|
||||
height: '8px',
|
||||
borderRadius: '50%',
|
||||
backgroundColor: 'white',
|
||||
}} />
|
||||
{isConnected && isAuthenticated ? '● Connected' : '○ Disconnected'}
|
||||
{wsError && <div style={{ fontSize: '10px', marginLeft: '8px' }}>{wsError}</div>}
|
||||
</div>
|
||||
);
|
||||
|
||||
// Показываем ошибку подключения
|
||||
if (!isConnected) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<CircularProgress />
|
||||
<Typography sx={{ ml: 2 }}>Подключение к серверу...</Typography>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
if (wsError) {
|
||||
return (
|
||||
<Box display="flex" justifyContent="center" alignItems="center" minHeight="400px">
|
||||
<Typography color="error">Ошибка подключения: {wsError}</Typography>
|
||||
</Box>
|
||||
);
|
||||
if (!data) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<ConnectionStatus />
|
||||
<SettingsPanel
|
||||
selectedColumn={selectedColumn}
|
||||
table={table}
|
||||
@ -303,34 +329,4 @@ const RealtimeTableContent = ({ formType, formId, sheetName }) => {
|
||||
);
|
||||
};
|
||||
|
||||
// Основной компонент с провайдером WebSocket
|
||||
const RealtimeTable = ({ formType, formId, sheetName }) => {
|
||||
const [direction, setDirection] = useState(null);
|
||||
|
||||
// Получаем direction из URL или пропсов если нужно
|
||||
useEffect(() => {
|
||||
// Пример получения direction из URL параметров
|
||||
const urlParams = new URLSearchParams(window.location.search);
|
||||
const dir = urlParams.get('direction');
|
||||
if (dir) {
|
||||
setDirection(dir);
|
||||
}
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<RealtimeProvider
|
||||
formId={formId}
|
||||
sheetName={sheetName}
|
||||
direction={direction}
|
||||
userId={null}
|
||||
>
|
||||
<RealtimeTableContent
|
||||
formType={formType}
|
||||
formId={formId}
|
||||
sheetName={sheetName}
|
||||
/>
|
||||
</RealtimeProvider>
|
||||
);
|
||||
};
|
||||
|
||||
export default React.memo(RealtimeTable);
|
||||
@ -1,176 +1,296 @@
|
||||
import React, { createContext, useContext, useCallback, useRef, useState } from 'react';
|
||||
import { useWebSocket } from '../hooks/useWebSocket';
|
||||
import { useCellLocks } from '../hooks/useCellLocks';
|
||||
import { Config } from '../../../conf/config';
|
||||
import React, {
|
||||
createContext,
|
||||
useContext,
|
||||
useCallback,
|
||||
useRef,
|
||||
useState,
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import { useCellLocks } from "../hooks/useCellLocks";
|
||||
|
||||
const RealtimeContext = createContext(null);
|
||||
|
||||
export const useRealtime = () => {
|
||||
const context = useContext(RealtimeContext);
|
||||
if (!context) {
|
||||
throw new Error('useRealtime must be used within RealtimeProvider');
|
||||
throw new Error("useRealtime must be used within RealtimeProvider");
|
||||
}
|
||||
return context;
|
||||
};
|
||||
|
||||
export const RealtimeProvider = ({ children, formId, sheetName, direction, userId }) => {
|
||||
export const RealtimeProvider = ({
|
||||
children,
|
||||
formId,
|
||||
sheetName,
|
||||
direction,
|
||||
userId,
|
||||
}) => {
|
||||
console.log(children, formId, sheetName, direction, userId);
|
||||
const [pendingUpdates, setPendingUpdates] = useState(new Map());
|
||||
// Формируем URL в соответствии с вашим бэкендом
|
||||
const wsUrl = `${process.env.REACT_APP_WS_URL || 'ws://localhost:8000'}/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`;
|
||||
|
||||
const { isConnected, sendMessage, login, error } = useWebSocket(wsUrl, handleMessage);
|
||||
|
||||
// Формируем URL
|
||||
const wsUrl = `${process.env.REACT_APP_WS_URL || "ws://localhost:8000"}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`;
|
||||
|
||||
const handleMessage = useCallback((data) => {
|
||||
console.log("Received message:", data);
|
||||
|
||||
// Обработка ошибок
|
||||
if (data.error) {
|
||||
console.error("Server error:", data.error);
|
||||
onErrorRef.current?.(data.error);
|
||||
return;
|
||||
}
|
||||
|
||||
// Обработка событий
|
||||
switch (data.event) {
|
||||
case "cell_updated":
|
||||
if (data.result && onCellUpdateRef.current) {
|
||||
const updatedCells = Array.isArray(data.result)
|
||||
? data.result
|
||||
: [data.result];
|
||||
onCellUpdateRef.current(updatedCells);
|
||||
}
|
||||
break;
|
||||
|
||||
case "row_added":
|
||||
if (data.result && onRowAddRef.current) {
|
||||
onRowAddRef.current(data.result);
|
||||
}
|
||||
break;
|
||||
|
||||
case "row_deleted":
|
||||
if (data.result && onRowDeleteRef.current) {
|
||||
onRowDeleteRef.current(data.result);
|
||||
}
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log("Unknown event:", data.event);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
||||
wsUrl,
|
||||
handleMessage,
|
||||
);
|
||||
const {
|
||||
isCellLocked,
|
||||
tryLockCell,
|
||||
unlockCell,
|
||||
getCellLockInfo,
|
||||
releaseAllLocks
|
||||
} = useCellLocks(userId);
|
||||
releaseAllLocks,
|
||||
} = useCellLocks(null);
|
||||
|
||||
const onCellUpdateRef = useRef(null);
|
||||
const onRowAddRef = useRef(null);
|
||||
const onRowDeleteRef = useRef(null);
|
||||
const onErrorRef = useRef(null);
|
||||
const onAuthSuccessRef = useRef(null);
|
||||
const authAttemptedRef = useRef(false);
|
||||
const reconnectTimerRef = useRef(null);
|
||||
|
||||
function handleMessage(data) {
|
||||
// Обработка ошибок от бэкенда
|
||||
if (data.error) {
|
||||
onErrorRef.current?.(data.error);
|
||||
return;
|
||||
}
|
||||
// Функция для получения токена из localStorage
|
||||
const getAccessToken = useCallback(() => {
|
||||
try {
|
||||
const token = localStorage.getItem("access_token");
|
||||
|
||||
// Ваш бэкенд возвращает result после успешной операции
|
||||
switch (data.event) {
|
||||
case 'cell_updated':
|
||||
if (data.result && onCellUpdateRef.current) {
|
||||
// Ваш бэкенд возвращает результат в виде списка обновленных ячеек
|
||||
// Формат: [{ line_id, column, value }]
|
||||
const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
|
||||
onCellUpdateRef.current(updatedCells);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'row_added':
|
||||
if (data.result && onRowAddRef.current) {
|
||||
// Ваш бэкенд возвращает добавленную строку
|
||||
onRowAddRef.current(data.result);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'row_deleted':
|
||||
if (data.result && onRowDeleteRef.current) {
|
||||
// Ваш бэкенд возвращает ID удаленной строки
|
||||
onRowDeleteRef.current(data.result);
|
||||
}
|
||||
break;
|
||||
|
||||
case 'user_login':
|
||||
console.log('User logged in:', data.user_id);
|
||||
break;
|
||||
|
||||
default:
|
||||
console.log('Unknown event:', data.event);
|
||||
}
|
||||
}
|
||||
|
||||
const updateCell = useCallback(async (rowId, columnId, value) => {
|
||||
// Пытаемся заблокировать ячейку
|
||||
if (!tryLockCell(rowId, columnId, userId)) {
|
||||
const lockInfo = getCellLockInfo(rowId, columnId);
|
||||
onErrorRef.current?.(`Ячейка редактируется пользователем ${lockInfo?.userId}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Формируем сообщение в формате вашего бэкенда
|
||||
const message = {
|
||||
event: 'cell_updated',
|
||||
data: {
|
||||
line_id: rowId,
|
||||
column: columnId,
|
||||
value: value
|
||||
if (!token) {
|
||||
console.warn("No access token found in localStorage");
|
||||
return null;
|
||||
}
|
||||
};
|
||||
|
||||
// Сохраняем pending update для оптимистичного UI
|
||||
const updateKey = `${rowId}_${columnId}`;
|
||||
setPendingUpdates(prev => new Map(prev).set(updateKey, { value, timestamp: Date.now() }));
|
||||
return token;
|
||||
} catch (error) {
|
||||
console.error("Error reading token from localStorage:", error);
|
||||
return null;
|
||||
}
|
||||
}, []);
|
||||
|
||||
const sent = sendMessage(message);
|
||||
|
||||
if (!sent) {
|
||||
// Откатываем блокировку если сообщение не отправлено
|
||||
unlockCell(rowId, columnId, userId);
|
||||
setPendingUpdates(prev => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.delete(updateKey);
|
||||
return newMap;
|
||||
});
|
||||
const sendLogin = useCallback(() => {
|
||||
const token = getAccessToken();
|
||||
if (!token) {
|
||||
console.error("Cannot login: no token available");
|
||||
onErrorRef.current?.("Токен авторизации не найден");
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [sendMessage, tryLockCell, unlockCell, getCellLockInfo, userId]);
|
||||
|
||||
const addRow = useCallback(async (rowData) => {
|
||||
const message = {
|
||||
event: 'row_added',
|
||||
data: rowData
|
||||
console.log("Sending login message with token");
|
||||
const loginMessage = {
|
||||
event: "user_login",
|
||||
data: { token },
|
||||
};
|
||||
|
||||
return sendMessage(message);
|
||||
}, [sendMessage]);
|
||||
|
||||
const deleteRow = useCallback(async (rowId) => {
|
||||
const message = {
|
||||
event: 'row_deleted',
|
||||
data: { row_id: rowId }
|
||||
};
|
||||
|
||||
return sendMessage(message);
|
||||
}, [sendMessage]);
|
||||
return sendMessage(loginMessage);
|
||||
}, [getAccessToken, sendMessage]);
|
||||
|
||||
useEffect(() => {
|
||||
if (isConnected && !authAttemptedRef.current) {
|
||||
authAttemptedRef.current = true;
|
||||
console.log("WebSocket connected, sending login...");
|
||||
setTimeout(() => {
|
||||
sendLogin();
|
||||
}, 100);
|
||||
}
|
||||
}, [isConnected, sendLogin]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isConnected) {
|
||||
authAttemptedRef.current = false;
|
||||
}
|
||||
}, [isConnected]);
|
||||
|
||||
const updateCell = useCallback(
|
||||
async (rowId, columnId, value) => {
|
||||
if (!isConnected) {
|
||||
onErrorRef.current?.("WebSocket не подключен");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!tryLockCell(rowId, columnId, userId)) {
|
||||
const lockInfo = getCellLockInfo(rowId, columnId);
|
||||
onErrorRef.current?.(
|
||||
`Ячейка редактируется пользователем ${lockInfo?.userId}`,
|
||||
);
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "cell_updated",
|
||||
data: {
|
||||
line_id: rowId,
|
||||
column: columnId,
|
||||
value: value,
|
||||
},
|
||||
};
|
||||
|
||||
const updateKey = `${rowId}_${columnId}`;
|
||||
setPendingUpdates((prev) =>
|
||||
new Map(prev).set(updateKey, { value, timestamp: Date.now() }),
|
||||
);
|
||||
|
||||
const sent = sendMessage(message);
|
||||
|
||||
if (!sent) {
|
||||
unlockCell(rowId, columnId, userId);
|
||||
setPendingUpdates((prev) => {
|
||||
const newMap = new Map(prev);
|
||||
newMap.delete(updateKey);
|
||||
return newMap;
|
||||
});
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
[
|
||||
isConnected,
|
||||
sendMessage,
|
||||
tryLockCell,
|
||||
unlockCell,
|
||||
getCellLockInfo,
|
||||
userId,
|
||||
],
|
||||
);
|
||||
|
||||
const addRow = useCallback(
|
||||
async (rowData) => {
|
||||
if (!isConnected) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "row_added",
|
||||
data: rowData,
|
||||
};
|
||||
|
||||
return sendMessage(message);
|
||||
},
|
||||
[isConnected, sendMessage],
|
||||
);
|
||||
|
||||
const deleteRow = useCallback(
|
||||
async (rowId) => {
|
||||
if (!isConnected ) {
|
||||
onErrorRef.current?.("Нет подключения или авторизации");
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: "row_deleted",
|
||||
data: { row_id: rowId },
|
||||
};
|
||||
|
||||
return sendMessage(message);
|
||||
},
|
||||
[isConnected, sendMessage],
|
||||
);
|
||||
|
||||
const subscribeToCellUpdates = useCallback((callback) => {
|
||||
onCellUpdateRef.current = callback;
|
||||
return () => { onCellUpdateRef.current = null; };
|
||||
return () => {
|
||||
onCellUpdateRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const subscribeToRowAdds = useCallback((callback) => {
|
||||
onRowAddRef.current = callback;
|
||||
return () => { onRowAddRef.current = null; };
|
||||
return () => {
|
||||
onRowAddRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const subscribeToRowDeletes = useCallback((callback) => {
|
||||
onRowDeleteRef.current = callback;
|
||||
return () => { onRowDeleteRef.current = null; };
|
||||
return () => {
|
||||
onRowDeleteRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const subscribeToErrors = useCallback((callback) => {
|
||||
onErrorRef.current = callback;
|
||||
return () => { onErrorRef.current = null; };
|
||||
return () => {
|
||||
onErrorRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const loginUser = useCallback((token) => {
|
||||
login(token);
|
||||
}, [login]);
|
||||
const subscribeToAuthSuccess = useCallback((callback) => {
|
||||
onAuthSuccessRef.current = callback;
|
||||
return () => {
|
||||
onAuthSuccessRef.current = null;
|
||||
};
|
||||
}, []);
|
||||
|
||||
const retryLogin = useCallback(() => {
|
||||
if (isConnected) {
|
||||
authAttemptedRef.current = false;
|
||||
sendLogin();
|
||||
}
|
||||
}, [isConnected, sendLogin]);
|
||||
|
||||
return (
|
||||
<RealtimeContext.Provider value={{
|
||||
isConnected,
|
||||
error,
|
||||
updateCell,
|
||||
addRow,
|
||||
deleteRow,
|
||||
isCellLocked: (rowId, columnId) => isCellLocked(rowId, columnId, userId),
|
||||
getCellLockInfo: (rowId, columnId) => getCellLockInfo(rowId, columnId),
|
||||
tryLockCell: (rowId, columnId) => tryLockCell(rowId, columnId, userId),
|
||||
subscribeToCellUpdates,
|
||||
subscribeToRowAdds,
|
||||
subscribeToRowDeletes,
|
||||
subscribeToErrors,
|
||||
login: loginUser,
|
||||
releaseAllLocks: () => releaseAllLocks(userId)
|
||||
}}>
|
||||
<RealtimeContext.Provider
|
||||
value={{
|
||||
isConnected,
|
||||
error: lastError,
|
||||
updateCell,
|
||||
addRow,
|
||||
deleteRow,
|
||||
isCellLocked: (rowId, columnId) =>
|
||||
isCellLocked(rowId, columnId, userId),
|
||||
getCellLockInfo: (rowId, columnId) => getCellLockInfo(rowId, columnId),
|
||||
tryLockCell: (rowId, columnId) => tryLockCell(rowId, columnId, userId),
|
||||
subscribeToCellUpdates,
|
||||
subscribeToRowAdds,
|
||||
subscribeToRowDeletes,
|
||||
subscribeToErrors,
|
||||
subscribeToAuthSuccess,
|
||||
retryLogin,
|
||||
releaseAllLocks: () => releaseAllLocks(userId),
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</RealtimeContext.Provider>
|
||||
);
|
||||
};
|
||||
};
|
||||
|
||||
165
web/src/components/RealtimeTable/hooks/useTableWebSocket.js
Normal file
165
web/src/components/RealtimeTable/hooks/useTableWebSocket.js
Normal file
@ -0,0 +1,165 @@
|
||||
// hooks/useTableWebSocket.js
|
||||
import { useWebSocket } from './useWebSocket';
|
||||
import { useCallback, useEffect, useRef } from 'react';
|
||||
|
||||
export const useTableWebSocket = (formId, sheet, direction = null) => {
|
||||
// Формируем URL WebSocket
|
||||
const wsUrl = `${process.env.REACT_APP_WS_URL || 'ws://localhost:8000'}/ws/form/${formId}/sheet/${sheet}${direction ? `?direction=${direction}` : ''}`;
|
||||
|
||||
const pendingChanges = useRef(new Map()); // Храним ожидающие подтверждения изменения
|
||||
const callbacks = useRef(new Map()); // Храним колбэки для каждого изменения
|
||||
|
||||
const handleWebSocketMessage = useCallback((data) => {
|
||||
console.log('Received WebSocket message:', data);
|
||||
|
||||
// Обработка подтверждения изменения
|
||||
if (data.event === 'cell_updated' && data.result) {
|
||||
const changeKey = `${data.data.line_id}_${data.data.column}`;
|
||||
const callback = callbacks.current.get(changeKey);
|
||||
|
||||
if (callback) {
|
||||
// Успешное обновление
|
||||
callback.success?.(data.result);
|
||||
callbacks.current.delete(changeKey);
|
||||
pendingChanges.current.delete(changeKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка ошибок
|
||||
if (data.error) {
|
||||
const changeKey = `${data.data?.line_id}_${data.data?.column}`;
|
||||
const callback = callbacks.current.get(changeKey);
|
||||
|
||||
if (callback) {
|
||||
callback.error?.(data.error);
|
||||
callbacks.current.delete(changeKey);
|
||||
pendingChanges.current.delete(changeKey);
|
||||
}
|
||||
}
|
||||
|
||||
// Обработка обновлений от других пользователей
|
||||
if (data.event === 'cell_updated' && data.result) {
|
||||
// Обновляем таблицу данными от других пользователей
|
||||
if (window.dispatchEvent) {
|
||||
window.dispatchEvent(new CustomEvent('table-data-update', {
|
||||
detail: {
|
||||
lineId: data.data.line_id,
|
||||
column: data.data.column,
|
||||
value: data.data.value,
|
||||
sourceUserId: data.user_id
|
||||
}
|
||||
}));
|
||||
}
|
||||
}
|
||||
}, []);
|
||||
|
||||
const { isConnected, error, sendMessage, login, disconnect } = useWebSocket(
|
||||
wsUrl,
|
||||
handleWebSocketMessage
|
||||
);
|
||||
|
||||
// Отправка обновления ячейки
|
||||
const updateCell = useCallback((lineId, column, value, onSuccess, onError) => {
|
||||
if (!isConnected) {
|
||||
onError?.('WebSocket not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: 'cell_updated',
|
||||
data: {
|
||||
line_id: lineId,
|
||||
column: column,
|
||||
value: value
|
||||
}
|
||||
};
|
||||
|
||||
const changeKey = `${lineId}_${column}`;
|
||||
|
||||
// Сохраняем колбэки
|
||||
callbacks.current.set(changeKey, { success: onSuccess, error: onError });
|
||||
pendingChanges.current.set(changeKey, message);
|
||||
|
||||
// Отправляем сообщение
|
||||
const sent = sendMessage(message);
|
||||
|
||||
if (!sent) {
|
||||
callbacks.current.delete(changeKey);
|
||||
pendingChanges.current.delete(changeKey);
|
||||
onError?.('Failed to send message');
|
||||
}
|
||||
|
||||
return sent;
|
||||
}, [isConnected, sendMessage]);
|
||||
|
||||
// Добавление строки
|
||||
const addRow = useCallback((rowData, onSuccess, onError) => {
|
||||
if (!isConnected) {
|
||||
onError?.('WebSocket not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: 'row_added',
|
||||
data: rowData
|
||||
};
|
||||
|
||||
const sent = sendMessage(message);
|
||||
|
||||
if (!sent) {
|
||||
onError?.('Failed to send message');
|
||||
}
|
||||
|
||||
return sent;
|
||||
}, [isConnected, sendMessage]);
|
||||
|
||||
// Удаление строки
|
||||
const deleteRow = useCallback((rowId, onSuccess, onError) => {
|
||||
if (!isConnected) {
|
||||
onError?.('WebSocket not connected');
|
||||
return false;
|
||||
}
|
||||
|
||||
const message = {
|
||||
event: 'row_deleted',
|
||||
data: {
|
||||
row_id: rowId
|
||||
}
|
||||
};
|
||||
|
||||
const sent = sendMessage(message);
|
||||
|
||||
if (!sent) {
|
||||
onError?.('Failed to send message');
|
||||
}
|
||||
|
||||
return sent;
|
||||
}, [isConnected, sendMessage]);
|
||||
|
||||
// Авторизация при подключении
|
||||
useEffect(() => {
|
||||
if (isConnected) {
|
||||
const token = localStorage.getItem('token'); // или откуда вы получаете токен
|
||||
if (token) {
|
||||
login(token);
|
||||
}
|
||||
}
|
||||
}, [isConnected, login]);
|
||||
|
||||
// Очистка при размонтировании
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
callbacks.current.clear();
|
||||
pendingChanges.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
isConnected,
|
||||
error,
|
||||
updateCell,
|
||||
addRow,
|
||||
deleteRow,
|
||||
disconnect
|
||||
};
|
||||
};
|
||||
@ -34,12 +34,12 @@ export const useWebSocket = (url, onMessage) => {
|
||||
|
||||
ws.onerror = (event) => {
|
||||
console.error('WebSocket error:', event);
|
||||
// Не закрываем соединение при ошибке, просто логируем
|
||||
console.error('WebSocket readyState:', ws.readyState);
|
||||
setError('Connection error occurred');
|
||||
};
|
||||
|
||||
ws.onclose = (event) => {
|
||||
console.log('WebSocket closed:', event.code, event.reason);
|
||||
console.log('WebSocket closed:', event.code, event.reason, 'Clean:', event.wasClean);
|
||||
setIsConnected(false);
|
||||
|
||||
// Не переподключаемся если закрыто intentionally или с ошибкой авторизации
|
||||
|
||||
@ -3,9 +3,18 @@ import { createPortal } from 'react-dom';
|
||||
import { columns as mockColumns } from '../../mocks/mockTable';
|
||||
import { config } from './constants/FORM_2/AHR';
|
||||
|
||||
function EditCellPortal({ cell, table, EditCell, onChange }) {
|
||||
function EditCellPortal({
|
||||
cell,
|
||||
table,
|
||||
EditCell,
|
||||
onChange,
|
||||
onSaveStart,
|
||||
onSaveEnd,
|
||||
onError
|
||||
}) {
|
||||
const ref = useRef(null);
|
||||
const [refTbody, setRefTbody] = useState(false);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const isDisabled = false;
|
||||
|
||||
useEffect(() => {
|
||||
@ -15,6 +24,60 @@ function EditCellPortal({ cell, table, EditCell, onChange }) {
|
||||
}
|
||||
}, []);
|
||||
|
||||
const handleChange = async (val) => {
|
||||
if (isSaving) return;
|
||||
|
||||
setIsSaving(true);
|
||||
onSaveStart?.();
|
||||
|
||||
// Сохраняем старое значение на случай ошибки
|
||||
const oldValue = cell.getValue();
|
||||
console.log(table.options.meta)
|
||||
|
||||
try {
|
||||
// Сначала обновляем локальное состояние (оптимистичное обновление)
|
||||
onChange(val);
|
||||
|
||||
// Используем updateCell из контекста (передается через meta)
|
||||
if (table.options.meta?.updateCell) {
|
||||
const success = await table.options.meta.updateCell(
|
||||
cell.row.id,
|
||||
cell.column.id,
|
||||
val
|
||||
);
|
||||
|
||||
if (success) {
|
||||
console.log('Cell updated successfully via WebSocket');
|
||||
onSaveEnd?.(true);
|
||||
table.setEditingCell(null);
|
||||
} else {
|
||||
console.error('Failed to update cell via WebSocket');
|
||||
const errorMsg = 'Не удалось сохранить изменение';
|
||||
onError?.(errorMsg);
|
||||
onSaveEnd?.(false);
|
||||
// Возвращаем предыдущее значение
|
||||
onChange(oldValue);
|
||||
}
|
||||
} else {
|
||||
// Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback)
|
||||
console.warn('WebSocket updateCell not configured in meta');
|
||||
if (table.options.meta?.onCellUpdate) {
|
||||
table.options.meta.onCellUpdate(cell.row.id, cell.column.id, val);
|
||||
}
|
||||
onSaveEnd?.(true);
|
||||
table.setEditingCell(null);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('Error updating cell:', error);
|
||||
onError?.(error.message);
|
||||
onSaveEnd?.(false);
|
||||
// Возвращаем предыдущее значение
|
||||
onChange(oldValue);
|
||||
} finally {
|
||||
setIsSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<div
|
||||
@ -24,54 +87,87 @@ function EditCellPortal({ cell, table, EditCell, onChange }) {
|
||||
height: '100%',
|
||||
}}
|
||||
/>
|
||||
{refTbody &&
|
||||
{refTbody && !isSaving &&
|
||||
createPortal(
|
||||
<EditCell
|
||||
refCell={ref}
|
||||
cell={cell}
|
||||
value={cell.getValue()}
|
||||
// onChange={setNewValue}
|
||||
disabled={isDisabled}
|
||||
onChange={(val) => {
|
||||
onChange(val);
|
||||
table.setEditingCell(null); // Закрываем после сохранения
|
||||
}}
|
||||
disabled={isDisabled || isSaving}
|
||||
onChange={handleChange}
|
||||
/>,
|
||||
refTbody,
|
||||
)}
|
||||
{isSaving && (
|
||||
createPortal(
|
||||
<div style={{
|
||||
position: 'absolute',
|
||||
background: 'rgba(0,0,0,0.5)',
|
||||
color: 'white',
|
||||
padding: '4px 8px',
|
||||
borderRadius: '4px',
|
||||
fontSize: '12px',
|
||||
zIndex: 1000
|
||||
}}>
|
||||
Saving...
|
||||
</div>,
|
||||
refTbody
|
||||
)
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
export const getTableColumns = ({ Cell, EditCell, columnsConfig }) => {
|
||||
|
||||
export const getTableColumns = ({
|
||||
Cell,
|
||||
EditCell,
|
||||
columnsConfig,
|
||||
onCellUpdateError
|
||||
}) => {
|
||||
const columnColors = { ...columnsConfig.colors };
|
||||
const columns = [...columnsConfig.columns];
|
||||
|
||||
const processColumns = (columns, EditCell, Cell) => {
|
||||
columns.forEach((col) => {
|
||||
col.Cell = ({ cell, table, column, row }) => (
|
||||
<>
|
||||
<Cell
|
||||
cell={cell}
|
||||
globalFilter={
|
||||
table.getState().globalFilter || column.getFilterValue()
|
||||
}
|
||||
backgroundColor={columnColors[column.id].color_type[row.original.row_type]}
|
||||
color={columnColors[column.id].color_type['COLOR']}
|
||||
/>
|
||||
</>
|
||||
|
||||
|
||||
<Cell
|
||||
row={row}
|
||||
column={column}
|
||||
cell={cell}
|
||||
globalFilter={
|
||||
table.getState().globalFilter || column.getFilterValue()
|
||||
}
|
||||
backgroundColor={columnColors[column.id]?.color_type?.[row.original?.row_type || row.row_type]}
|
||||
color={columnColors[column.id]?.color_type?.['COLOR']}
|
||||
isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]}
|
||||
/>
|
||||
);
|
||||
|
||||
col.Edit = ({ cell, table, row, column }) => (
|
||||
<EditCellPortal
|
||||
cell={cell}
|
||||
table={table}
|
||||
EditCell={EditCell}
|
||||
onChange={(value) => {
|
||||
row._valuesCache[column.id] = value;
|
||||
// Локальное обновление кэша
|
||||
if (row._valuesCache) {
|
||||
row._valuesCache[column.id] = value;
|
||||
}
|
||||
}}
|
||||
onSaveStart={() => {
|
||||
if (table.options.meta?.setUpdatingCell) {
|
||||
table.options.meta.setUpdatingCell(row.id, column.id, true);
|
||||
}
|
||||
}}
|
||||
onSaveEnd={(success) => {
|
||||
if (table.options.meta?.setUpdatingCell) {
|
||||
table.options.meta.setUpdatingCell(row.id, column.id, false);
|
||||
}
|
||||
if (!success && onCellUpdateError) {
|
||||
onCellUpdateError(row.id, column.id, 'Failed to save');
|
||||
}
|
||||
}}
|
||||
onError={onCellUpdateError}
|
||||
/>
|
||||
);
|
||||
|
||||
@ -96,4 +192,4 @@ export const getTableColumns = ({ Cell, EditCell, columnsConfig }) => {
|
||||
};
|
||||
|
||||
return [rowNumber, ...columns];
|
||||
};
|
||||
};
|
||||
@ -5,6 +5,8 @@ import RealtimeTable from '../components/RealtimeTable';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { useEffect } from 'react';
|
||||
import { } from '../components/RealtimeTable/'
|
||||
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
||||
import { useAuth } from '../app/context/AuthProvider';
|
||||
|
||||
const TableInfo = () => {
|
||||
const portalContent = (
|
||||
@ -23,10 +25,21 @@ const TableInfo = () => {
|
||||
|
||||
export default function NewTablePage() {
|
||||
const { formId, formType, sheetName } = useParams()
|
||||
const { user } = useAuth();
|
||||
|
||||
return (
|
||||
<>
|
||||
<RealtimeTable formType={formType} formId={formId} sheetName={sheetName} />
|
||||
{user && (
|
||||
<RealtimeProvider
|
||||
formId={formId}
|
||||
sheetName={sheetName}
|
||||
userId={user.id}
|
||||
>
|
||||
<RealtimeTable formType={formType} formId={formId} sheetName={sheetName} />
|
||||
|
||||
</RealtimeProvider>
|
||||
)}
|
||||
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@ -2,6 +2,7 @@ import { createPortal } from 'react-dom';
|
||||
import { TaskInfoContainer } from '../components/common/SwitchFormTask/SwitchFormTask.style';
|
||||
|
||||
import RealtimeTable from '../components/RealtimeTable';
|
||||
import { RealtimeProvider } from '../components/RealtimeTable/contexts/RealtimeContext';
|
||||
|
||||
const TableInfo = () => {
|
||||
const portalContent = (
|
||||
@ -21,7 +22,9 @@ const TableInfo = () => {
|
||||
export default function NewTablePage() {
|
||||
return (
|
||||
<>
|
||||
<RealtimeTable />
|
||||
|
||||
<RealtimeTable
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user