блокировка ячейки веб сокеты
This commit is contained in:
parent
596ec13943
commit
c8165e8ca9
@ -12,11 +12,28 @@ const ContainerForText = styled.span`
|
|||||||
white-space: pre-wrap;
|
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) => {
|
const highlightText = (text, searchQuery) => {
|
||||||
if (!searchQuery || !text) return text;
|
if (!searchQuery || !text) return text;
|
||||||
|
|
||||||
// Экранируем спецсимволы в поисковом запросе
|
|
||||||
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
const escapedQuery = searchQuery.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
||||||
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
const regex = new RegExp(`(${escapedQuery})`, 'gi');
|
||||||
|
|
||||||
@ -49,7 +66,6 @@ const formatNumber = (value) => {
|
|||||||
const num = Number(value);
|
const num = Number(value);
|
||||||
if (isNaN(num)) return String(value);
|
if (isNaN(num)) return String(value);
|
||||||
|
|
||||||
// Форматируем число с 1 знаком после запятой и разделением по 3 цифры
|
|
||||||
return num.toLocaleString('ru-RU', {
|
return num.toLocaleString('ru-RU', {
|
||||||
minimumFractionDigits: 1,
|
minimumFractionDigits: 1,
|
||||||
maximumFractionDigits: 1,
|
maximumFractionDigits: 1,
|
||||||
@ -58,21 +74,34 @@ const formatNumber = (value) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundColor, color, isEditable }) => {
|
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();
|
const cellValue = cell.getValue();
|
||||||
let textValue = String(cellValue || '');
|
let textValue = String(cellValue || '');
|
||||||
|
|
||||||
// Проверяем, является ли значение числом, и форматируем его
|
|
||||||
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
const isNumeric = !isNaN(Number(cellValue)) && cellValue !== null && cellValue !== undefined && cellValue !== '';
|
||||||
if (isNumeric) {
|
if (isNumeric) {
|
||||||
textValue = formatNumber(cellValue);
|
textValue = formatNumber(cellValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Подсвечиваем текст с учетом поискового запроса
|
|
||||||
const highlightedContent = useMemo(() => {
|
const highlightedContent = useMemo(() => {
|
||||||
return highlightText(textValue, globalFilter);
|
return highlightText(textValue, globalFilter);
|
||||||
}, [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',
|
border: '2px solid #d3d3d3',
|
||||||
color: '#525252',
|
color: '#525252',
|
||||||
cursor: 'not-allowed',
|
cursor: 'not-allowed',
|
||||||
@ -95,13 +124,19 @@ const Cell = React.memo(({ cell, row, column, onClick, globalFilter, backgroundC
|
|||||||
bottom: 0,
|
bottom: 0,
|
||||||
border: '2px solid transparent',
|
border: '2px solid transparent',
|
||||||
padding: '8px',
|
padding: '8px',
|
||||||
backgroundColor: backgroundColor,
|
backgroundColor: isLocked ? '#f5f5f5' : backgroundColor,
|
||||||
color: color,
|
color: isLocked ? '#999999' : color,
|
||||||
...lockedStyles
|
...lockedStyles,
|
||||||
|
...editableStyles,
|
||||||
}}
|
}}
|
||||||
onClick={onClick}
|
onClick={isLocked ? undefined : onClick}
|
||||||
>
|
>
|
||||||
<ContainerForText>{highlightedContent}</ContainerForText>
|
<ContainerForText>{highlightedContent}</ContainerForText>
|
||||||
|
{isLocked && (
|
||||||
|
<LockBadge>
|
||||||
|
🔒
|
||||||
|
</LockBadge>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
|
|||||||
@ -147,8 +147,8 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
scrollPaddingEnd: 0,
|
scrollPaddingEnd: 0,
|
||||||
},
|
},
|
||||||
onEditingCellChange: (cell) => {
|
onEditingCellChange: (cell) => {
|
||||||
console.log(cell, 'CELL')
|
|
||||||
if (cell) {
|
if (cell) {
|
||||||
|
if (editingCell) return;
|
||||||
contextStartEditing?.(cell.row, cell.column);
|
contextStartEditing?.(cell.row, cell.column);
|
||||||
} else {
|
} else {
|
||||||
contextEndEditing?.(editingCell.row, editingCell.column);
|
contextEndEditing?.(editingCell.row, editingCell.column);
|
||||||
@ -226,7 +226,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
|
|||||||
const table = useMaterialReactTable(tableConfig);
|
const table = useMaterialReactTable(tableConfig);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!dataEditingCells.line_id) {
|
if (!dataEditingCells || !dataEditingCells.line_id) {
|
||||||
setEditingCell(null);
|
setEditingCell(null);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@ -7,7 +7,6 @@ import React, {
|
|||||||
useEffect,
|
useEffect,
|
||||||
} from "react";
|
} from "react";
|
||||||
import { useWebSocket } from "../hooks/useWebSocket";
|
import { useWebSocket } from "../hooks/useWebSocket";
|
||||||
import { useCellLocks } from "../hooks/useCellLocks";
|
|
||||||
import { toast } from "react-toastify";
|
import { toast } from "react-toastify";
|
||||||
|
|
||||||
const RealtimeContext = createContext(null);
|
const RealtimeContext = createContext(null);
|
||||||
@ -29,7 +28,8 @@ export const RealtimeProvider = ({
|
|||||||
}) => {
|
}) => {
|
||||||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
// Формируем 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 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) => {
|
const handleMessage = useCallback((data) => {
|
||||||
// Обработка ошибок
|
// Обработка ошибок
|
||||||
if (data.error) {
|
if (data.error) {
|
||||||
@ -83,13 +83,6 @@ export const RealtimeProvider = ({
|
|||||||
wsUrl,
|
wsUrl,
|
||||||
handleMessage,
|
handleMessage,
|
||||||
);
|
);
|
||||||
const {
|
|
||||||
isCellLocked,
|
|
||||||
tryLockCell,
|
|
||||||
unlockCell,
|
|
||||||
getCellLockInfo,
|
|
||||||
releaseAllLocks,
|
|
||||||
} = useCellLocks(null);
|
|
||||||
|
|
||||||
const onCellUpdateRef = useRef(null);
|
const onCellUpdateRef = useRef(null);
|
||||||
const onRowAddRef = useRef(null);
|
const onRowAddRef = useRef(null);
|
||||||
@ -350,18 +343,16 @@ export const RealtimeProvider = ({
|
|||||||
updateCell,
|
updateCell,
|
||||||
addRow,
|
addRow,
|
||||||
deleteRow,
|
deleteRow,
|
||||||
isCellLocked: (rowId, columnId) =>
|
|
||||||
isCellLocked(rowId, columnId, userId),
|
|
||||||
getCellLockInfo: (rowId, columnId) => getCellLockInfo(rowId, columnId),
|
|
||||||
tryLockCell: (rowId, columnId) => tryLockCell(rowId, columnId, userId),
|
|
||||||
subscribeToCellUpdates,
|
|
||||||
subscribeToRowAdds,
|
subscribeToRowAdds,
|
||||||
subscribeToRowDeletes,
|
subscribeToRowDeletes,
|
||||||
subscribeToErrors,
|
subscribeToErrors,
|
||||||
subscribeToAuthSuccess,
|
subscribeToAuthSuccess,
|
||||||
subscribeToCellEditStart,
|
subscribeToCellEditStart,
|
||||||
subscribeToCellEditEnd,
|
subscribeToCellEditEnd,
|
||||||
|
subscribeToCellUpdates,
|
||||||
retryLogin,
|
retryLogin,
|
||||||
|
lockedCells,
|
||||||
|
setLockedCells,
|
||||||
releaseAllLocks: () => releaseAllLocks(userId),
|
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,
|
subscribeToErrors,
|
||||||
subscribeToCellEditStart,
|
subscribeToCellEditStart,
|
||||||
subscribeToCellEditEnd,
|
subscribeToCellEditEnd,
|
||||||
|
setLockedCells,
|
||||||
} = useRealtime();
|
} = useRealtime();
|
||||||
|
|
||||||
const formatedToSubRows = useCallback((depth, oldData) => {
|
const formatedToSubRows = useCallback((depth, oldData) => {
|
||||||
@ -208,96 +209,28 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
|||||||
|
|
||||||
// Обработчик начала редактирования ячейки
|
// Обработчик начала редактирования ячейки
|
||||||
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
|
const unsubscribeCellEditStart = subscribeToCellEditStart((data) => {
|
||||||
console.log(data);
|
|
||||||
console.log('START EDIT');
|
|
||||||
const dataCell = data.data;
|
const dataCell = data.data;
|
||||||
dataCell.column = 'data.'+ dataCell.column;
|
dataCell.column = "data." + dataCell.column;
|
||||||
setEditingCells(data.data)
|
if (data.is_self) {
|
||||||
|
setEditingCells(data.data);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
// const { line_id, column } = data.data;
|
const { line_id, column } = dataCell;
|
||||||
// const cellKey = `${line_id}_${column}`;
|
const cellKey = `${line_id}_${column}`;
|
||||||
|
|
||||||
// // Сохраняем информацию о том, кто начал редактирование
|
// Сохраняем информацию о том, кто начал редактирование
|
||||||
// setEditingCells((prev) => ({
|
setLockedCells((prev) => [...prev, cellKey]);
|
||||||
// ...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}`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
// Обработчик завершения редактирования ячейки
|
// Обработчик завершения редактирования ячейки
|
||||||
const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => {
|
const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => {
|
||||||
setEditingCells(null);
|
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}`;
|
const cellKey = `${line_id}_${column}`;
|
||||||
|
setLockedCells((prev) => prev.filter((c) => c !== cellKey));
|
||||||
// Удаляем информацию о редактировании
|
|
||||||
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`);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const unsubscribeErrors = subscribeToErrors((err) => {
|
const unsubscribeErrors = subscribeToErrors((err) => {
|
||||||
@ -344,24 +277,6 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
|||||||
[formatedToSubRows],
|
[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 {
|
return {
|
||||||
data,
|
data,
|
||||||
setData,
|
setData,
|
||||||
@ -369,8 +284,6 @@ const useRealtimeData = (formId, sheetName, direction) => {
|
|||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
editingCells,
|
editingCells,
|
||||||
isCellEditing,
|
|
||||||
getCellEditingInfo,
|
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user