блокировка ячейки веб сокеты
This commit is contained in:
parent
596ec13943
commit
c8165e8ca9
@ -12,11 +12,28 @@ const ContainerForText = styled.span`
|
||||
white-space: pre-wrap;
|
||||
`;
|
||||
|
||||
// Стилизованный компонент для плашки блокировки
|
||||
const LockBadge = styled.div`
|
||||
position: absolute;
|
||||
top: 2px;
|
||||
right: 2px;
|
||||
background: rgba(0, 0, 0, 0.7);
|
||||
color: white;
|
||||
font-size: 9px;
|
||||
padding: 1px 4px;
|
||||
border-radius: 3px;
|
||||
pointer-events: none;
|
||||
z-index: 10;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
|
||||
letter-spacing: 0.3px;
|
||||
backdrop-filter: blur(4px);
|
||||
border: 1px solid rgba(255, 255, 255, 0.1);
|
||||
`;
|
||||
|
||||
// Функция для подсветки текста
|
||||
const highlightText = (text, searchQuery) => {
|
||||
if (!searchQuery || !text) return text;
|
||||
|
||||
// Экранируем спецсимволы в поисковом запросе
|
||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||
|
||||
@ -49,7 +66,6 @@ const formatNumber = (value) => {
|
||||
const num = Number(value);
|
||||
if (isNaN(num)) return String(value);
|
||||
|
||||
// Форматируем число с 1 знаком после запятой и разделением по 3 цифры
|
||||
return num.toLocaleString('ru-RU', {
|
||||
minimumFractionDigits: 1,
|
||||
maximumFractionDigits: 1,
|
||||
@ -58,21 +74,34 @@ const formatNumber = (value) => {
|
||||
};
|
||||
|
||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
|
||||
const { lockedCells } = useRealtime();
|
||||
const cellKey = `${row.id}_${column.id}`;
|
||||
|
||||
const isLocked = lockedCells.includes(cellKey);
|
||||
|
||||
const cellValue = cell.getValue();
|
||||
let textValue = String(cellValue || '');
|
||||
|
||||
// Проверяем, является ли значение числом, и форматируем его
|
||||
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
||||
if (isNumeric) {
|
||||
textValue = formatNumber(cellValue);
|
||||
}
|
||||
|
||||
// Подсвечиваем текст с учетом поискового запроса
|
||||
const highlightedContent = useMemo(() => {
|
||||
return highlightText(textValue, globalFilter);
|
||||
}, [textValue, globalFilter]);
|
||||
|
||||
const lockedStyles = !isEditable ? {
|
||||
// Базовые стили для заблокированной ячейки
|
||||
const lockedStyles = isLocked ? {
|
||||
border: '2px solid #e0e0e0',
|
||||
backgroundColor: '#f5f5f5',
|
||||
color: '#999999',
|
||||
cursor: 'not-allowed',
|
||||
opacity: 0.85,
|
||||
} : {};
|
||||
|
||||
// Если isEditable false, добавляем дополнительные стили
|
||||
const editableStyles = !isEditable ? {
|
||||
border: '2px solid #d3d3d3',
|
||||
color: '#525252',
|
||||
cursor: 'not-allowed',
|
||||
@ -95,13 +124,19 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
|
||||
bottom: 0,
|
||||
border: '2px solid transparent',
|
||||
padding: '8px',
|
||||
backgroundColor: backgroundColor,
|
||||
color: color,
|
||||
...lockedStyles
|
||||
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||
color: isLocked ? '#999999' : color,
|
||||
...lockedStyles,
|
||||
...editableStyles,
|
||||
}}
|
||||
onClick={onClick}
|
||||
onClick={isLocked ? undefined : onClick}
|
||||
>
|
||||
<ContainerForText>{highlightedContent}</ContainerForText>
|
||||
{isLocked && (
|
||||
<LockBadge>
|
||||
🔒
|
||||
</LockBadge>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
});
|
||||
|
||||
@ -147,8 +147,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
||||
scrollPaddingEnd: 0,
|
||||
},
|
||||
onEditingCellChange: (cell) => {
|
||||
console.log(cell, 'CELL')
|
||||
if (cell) {
|
||||
if (editingCell) return;
|
||||
contextStartEditing?.(cell.row, cell.column);
|
||||
} else {
|
||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||
@ -226,7 +226,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
||||
const table = useMaterialReactTable(tableConfig);
|
||||
|
||||
useEffect(() => {
|
||||
if (!dataEditingCells.line_id) {
|
||||
if (!dataEditingCells || !dataEditingCells.line_id) {
|
||||
setEditingCell(null);
|
||||
return;
|
||||
}
|
||||
|
||||
@ -7,7 +7,6 @@ import React, {
|
||||
useEffect,
|
||||
} from "react";
|
||||
import { useWebSocket } from "../hooks/useWebSocket";
|
||||
import { useCellLocks } from "../hooks/useCellLocks";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
const RealtimeContext = createContext(null);
|
||||
@ -29,7 +28,8 @@ export const RealtimeProvider = ({
|
||||
}) => {
|
||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||||
const wsUrl = `${(process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || "ws://localhost:8000").replace(/^http/, "ws")}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`;
|
||||
|
||||
//для ячеек которые редактируются другими пользователями
|
||||
const [lockedCells, setLockedCells] = useState([]);
|
||||
const handleMessage = useCallback((data) => {
|
||||
// Обработка ошибок
|
||||
if (data.error) {
|
||||
@ -83,13 +83,6 @@ export const RealtimeProvider = ({
|
||||
wsUrl,
|
||||
handleMessage,
|
||||
);
|
||||
const {
|
||||
isCellLocked,
|
||||
tryLockCell,
|
||||
unlockCell,
|
||||
getCellLockInfo,
|
||||
releaseAllLocks,
|
||||
} = useCellLocks(null);
|
||||
|
||||
const onCellUpdateRef = useRef(null);
|
||||
const onRowAddRef = useRef(null);
|
||||
@ -350,18 +343,16 @@ export const RealtimeProvider = ({
|
||||
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,
|
||||
subscribeToCellEditStart,
|
||||
subscribeToCellEditEnd,
|
||||
subscribeToCellUpdates,
|
||||
retryLogin,
|
||||
lockedCells,
|
||||
setLockedCells,
|
||||
releaseAllLocks: () => releaseAllLocks(userId),
|
||||
}}
|
||||
>
|
||||
|
||||
@ -1,122 +0,0 @@
|
||||
import { useState, useCallback, useRef, useEffect } from 'react';
|
||||
|
||||
export const useCellLocks = (userId, lockTimeout = 30000) => {
|
||||
const [lockedCells, setLockedCells] = useState(new Map());
|
||||
const lockTimersRef = useRef(new Map());
|
||||
|
||||
const getCellKey = (rowId, columnId) => `${rowId}_${columnId}`;
|
||||
|
||||
const lockCell = useCallback((rowId, columnId, lockingUserId) => {
|
||||
const cellKey = getCellKey(rowId, columnId);
|
||||
|
||||
setLockedCells(prev => {
|
||||
const newLocks = new Map(prev);
|
||||
const existingLock = newLocks.get(cellKey);
|
||||
|
||||
if (existingLock && existingLock.userId !== lockingUserId) {
|
||||
return prev; // Cell already locked by another user
|
||||
}
|
||||
|
||||
newLocks.set(cellKey, {
|
||||
userId: lockingUserId,
|
||||
lockedAt: Date.now(),
|
||||
rowId,
|
||||
columnId
|
||||
});
|
||||
|
||||
return newLocks;
|
||||
});
|
||||
|
||||
return true;
|
||||
}, []);
|
||||
|
||||
const unlockCell = useCallback((rowId, columnId, unlockingUserId) => {
|
||||
const cellKey = getCellKey(rowId, columnId);
|
||||
|
||||
setLockedCells(prev => {
|
||||
const newLocks = new Map(prev);
|
||||
const lock = newLocks.get(cellKey);
|
||||
|
||||
if (lock && lock.userId === unlockingUserId) {
|
||||
newLocks.delete(cellKey);
|
||||
}
|
||||
|
||||
return newLocks;
|
||||
});
|
||||
|
||||
// Clear timer if exists
|
||||
if (lockTimersRef.current.has(cellKey)) {
|
||||
clearTimeout(lockTimersRef.current.get(cellKey));
|
||||
lockTimersRef.current.delete(cellKey);
|
||||
}
|
||||
}, []);
|
||||
|
||||
const tryLockCell = useCallback((rowId, columnId, currentUserId) => {
|
||||
const cellKey = getCellKey(rowId, columnId);
|
||||
const existingLock = lockedCells.get(cellKey);
|
||||
|
||||
if (existingLock && existingLock.userId !== currentUserId) {
|
||||
return false; // Cell is locked by another user
|
||||
}
|
||||
|
||||
if (!existingLock) {
|
||||
lockCell(rowId, columnId, currentUserId);
|
||||
|
||||
// Auto-unlock after timeout
|
||||
const timer = setTimeout(() => {
|
||||
unlockCell(rowId, columnId, currentUserId);
|
||||
}, lockTimeout);
|
||||
|
||||
lockTimersRef.current.set(cellKey, timer);
|
||||
}
|
||||
|
||||
return true;
|
||||
}, [lockedCells, lockCell, unlockCell, lockTimeout]);
|
||||
|
||||
const releaseAllLocks = useCallback((userId) => {
|
||||
setLockedCells(prev => {
|
||||
const newLocks = new Map(prev);
|
||||
for (const [key, lock] of newLocks.entries()) {
|
||||
if (lock.userId === userId) {
|
||||
newLocks.delete(key);
|
||||
if (lockTimersRef.current.has(key)) {
|
||||
clearTimeout(lockTimersRef.current.get(key));
|
||||
lockTimersRef.current.delete(key);
|
||||
}
|
||||
}
|
||||
}
|
||||
return newLocks;
|
||||
});
|
||||
}, []);
|
||||
|
||||
const isCellLocked = useCallback((rowId, columnId, currentUserId) => {
|
||||
const cellKey = getCellKey(rowId, columnId);
|
||||
const lock = lockedCells.get(cellKey);
|
||||
return lock && lock.userId !== currentUserId;
|
||||
}, [lockedCells]);
|
||||
|
||||
const getCellLockInfo = useCallback((rowId, columnId) => {
|
||||
const cellKey = getCellKey(rowId, columnId);
|
||||
return lockedCells.get(cellKey);
|
||||
}, [lockedCells]);
|
||||
|
||||
useEffect(() => {
|
||||
return () => {
|
||||
// Clear all timers on unmount
|
||||
for (const timer of lockTimersRef.current.values()) {
|
||||
clearTimeout(timer);
|
||||
}
|
||||
lockTimersRef.current.clear();
|
||||
};
|
||||
}, []);
|
||||
|
||||
return {
|
||||
lockedCells,
|
||||
lockCell,
|
||||
unlockCell,
|
||||
tryLockCell,
|
||||
isCellLocked,
|
||||
getCellLockInfo,
|
||||
releaseAllLocks
|
||||
};
|
||||
};
|
||||
@ -20,6 +20,7 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
||||
subscribeToErrors,
|
||||
subscribeToCellEditStart,
|
||||
subscribeToCellEditEnd,
|
||||
setLockedCells,
|
||||
} = useRealtime();
|
||||
|
||||
const formatedToSubRows = useCallback((depth, oldData) => {
|
||||
@ -208,96 +209,28 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
||||
|
||||
// Обработчик начала редактирования ячейки
|
||||
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
|
||||
console.log(data);
|
||||
console.log('START EDIT');
|
||||
const dataCell = data.data;
|
||||
dataCell.column = 'data.'+ dataCell.column;
|
||||
setEditingCells(data.data)
|
||||
dataCell.column = "data." + dataCell.column;
|
||||
if (data.is_self) {
|
||||
setEditingCells(data.data);
|
||||
return;
|
||||
}
|
||||
|
||||
// const { line_id, column } = data.data;
|
||||
// const cellKey = `${line_id}_${column}`;
|
||||
const { line_id, column } = dataCell;
|
||||
const cellKey = `${line_id}_${column}`;
|
||||
|
||||
// // Сохраняем информацию о том, кто начал редактирование
|
||||
// setEditingCells((prev) => ({
|
||||
// ...prev,
|
||||
// [cellKey]: {
|
||||
// userId: data.user_id || "unknown",
|
||||
// timestamp: Date.now(),
|
||||
// },
|
||||
// }));
|
||||
|
||||
// // Можно также добавить визуальную индикацию в данных
|
||||
// setData((prevData) => {
|
||||
// // Функция для обновления статуса редактирования в данных
|
||||
// const updateCellEditingStatus = (items) => {
|
||||
// return items.map((item) => {
|
||||
// if (item.line_id === line_id) {
|
||||
// // Находим нужную колонку и добавляем флаг редактирования
|
||||
// const updatedItem = { ...item };
|
||||
// if (updatedItem.columns && updatedItem.columns[column]) {
|
||||
// updatedItem.columns[column] = {
|
||||
// ...updatedItem.columns[column],
|
||||
// isEditing: true,
|
||||
// editingBy: data.user_id,
|
||||
// };
|
||||
// }
|
||||
// return updatedItem;
|
||||
// }
|
||||
// if (item.subRows) {
|
||||
// return {
|
||||
// ...item,
|
||||
// subRows: updateCellEditingStatus(item.subRows),
|
||||
// };
|
||||
// }
|
||||
// return item;
|
||||
// });
|
||||
// };
|
||||
// return updateCellEditingStatus(prevData);
|
||||
// });
|
||||
|
||||
// console.log(`Cell ${cellKey} editing started by user ${data.user_id}`);
|
||||
// Сохраняем информацию о том, кто начал редактирование
|
||||
setLockedCells((prev) => [...prev, cellKey]);
|
||||
});
|
||||
|
||||
// Обработчик завершения редактирования ячейки
|
||||
const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => {
|
||||
setEditingCells(null);
|
||||
const { line_id, column } = data.data;
|
||||
const dataCell = data.data;
|
||||
dataCell.column = "data." + dataCell.column;
|
||||
const { line_id, column } = dataCell;
|
||||
const cellKey = `${line_id}_${column}`;
|
||||
|
||||
// Удаляем информацию о редактировании
|
||||
setEditingCells((prev) => {
|
||||
const newState = { ...prev };
|
||||
delete newState[cellKey];
|
||||
return newState;
|
||||
});
|
||||
|
||||
// Убираем визуальную индикацию в данных
|
||||
setData((prevData) => {
|
||||
const updateCellEditingStatus = (items) => {
|
||||
return items.map((item) => {
|
||||
if (item.line_id === line_id) {
|
||||
const updatedItem = { ...item };
|
||||
if (updatedItem.columns && updatedItem.columns[column]) {
|
||||
// Удаляем флаги редактирования
|
||||
const { isEditing, editingBy, ...restColumns } =
|
||||
updatedItem.columns[column];
|
||||
updatedItem.columns[column] = restColumns;
|
||||
}
|
||||
return updatedItem;
|
||||
}
|
||||
if (item.subRows) {
|
||||
return {
|
||||
...item,
|
||||
subRows: updateCellEditingStatus(item.subRows),
|
||||
};
|
||||
}
|
||||
return item;
|
||||
});
|
||||
};
|
||||
return updateCellEditingStatus(prevData);
|
||||
});
|
||||
|
||||
console.log(`Cell ${cellKey} editing ended`);
|
||||
setLockedCells((prev) => prev.filter((c) => c !== cellKey));
|
||||
});
|
||||
|
||||
const unsubscribeErrors = subscribeToErrors((err) => {
|
||||
@ -344,24 +277,6 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
||||
[formatedToSubRows],
|
||||
);
|
||||
|
||||
// Функция для проверки, редактируется ли ячейка
|
||||
const isCellEditing = useCallback(
|
||||
(lineId, column) => {
|
||||
const cellKey = `${lineId}_${column}`;
|
||||
return !!editingCells[cellKey];
|
||||
},
|
||||
[editingCells],
|
||||
);
|
||||
|
||||
// Функция для получения информации о том, кто редактирует ячейку
|
||||
const getCellEditingInfo = useCallback(
|
||||
(lineId, column) => {
|
||||
const cellKey = `${lineId}_${column}`;
|
||||
return editingCells[cellKey] || null;
|
||||
},
|
||||
[editingCells],
|
||||
);
|
||||
|
||||
return {
|
||||
data,
|
||||
setData,
|
||||
@ -369,8 +284,6 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
||||
isLoading,
|
||||
error,
|
||||
editingCells,
|
||||
isCellEditing,
|
||||
getCellEditingInfo,
|
||||
};
|
||||
};
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user