diff --git a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
index d3682b2..5ef127f 100644
--- a/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
+++ b/web/src/components/RealtimeTable/Cell/Cell/Cell.jsx
@@ -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}
>
{highlightedContent}
+ {isLocked && (
+
+ 🔒
+
+ )}
);
});
diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx
index 51f678d..5a16d67 100644
--- a/web/src/components/RealtimeTable/RealtimeTable.jsx
+++ b/web/src/components/RealtimeTable/RealtimeTable.jsx
@@ -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;
}
diff --git a/web/src/components/RealtimeTable/contexts/RealtimeContext.js b/web/src/components/RealtimeTable/contexts/RealtimeContext.js
index b9ba118..9731607 100644
--- a/web/src/components/RealtimeTable/contexts/RealtimeContext.js
+++ b/web/src/components/RealtimeTable/contexts/RealtimeContext.js
@@ -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),
}}
>
diff --git a/web/src/components/RealtimeTable/hooks/useCellLocks.js b/web/src/components/RealtimeTable/hooks/useCellLocks.js
deleted file mode 100644
index 0a9d659..0000000
--- a/web/src/components/RealtimeTable/hooks/useCellLocks.js
+++ /dev/null
@@ -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
- };
-};
\ No newline at end of file
diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js
index 4aa9ac7..7ca18da 100644
--- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js
+++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js
@@ -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,
};
};