web сокеты для начала/конца редактирования ячейки

This commit is contained in:
PotapovaA 2026-06-22 17:16:29 +03:00
parent ca11d55228
commit aa18a8e19c
4 changed files with 261 additions and 19 deletions

View File

@ -25,13 +25,14 @@ import { toast } from 'react-toastify';
import { CircularProgress } from '@mui/material'; import { CircularProgress } from '@mui/material';
const RealtimeTable = ({ formType, formId, sheetName, direction }) => { const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { data, setData, isLoading: isTableLoading } = useRealtimeData(formId, sheetName, direction); const { data, setData, isLoading: isTableLoading, editingCells: dataEditingCells } = useRealtimeData(formId, sheetName, direction);
const [globalFilter, setGlobalFilter] = useState(''); const [globalFilter, setGlobalFilter] = useState('');
const [showColumnFilters, setShowColumnFilters] = useState(false); const [showColumnFilters, setShowColumnFilters] = useState(false);
const [selectedColumn, setSelectedColumn] = useState(); const [selectedColumn, setSelectedColumn] = useState();
const [rowSelection, setRowSelection] = useState({}); const [rowSelection, setRowSelection] = useState({});
const [isLoadingData, setIsLoadingData] = useState(false); const [isLoadingData, setIsLoadingData] = useState(false);
const [isPending, startTransition] = useTransition(); const [isPending, startTransition] = useTransition();
const [editingCell, setEditingCell] = useState(null);
const handleGlobalFilterChange = (value) => { const handleGlobalFilterChange = (value) => {
startTransition(() => { startTransition(() => {
@ -42,13 +43,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const { const {
isConnected, isConnected,
isAuthenticated, isAuthenticated,
subscribeToCellUpdates,
subscribeToErrors, subscribeToErrors,
subscribeToAuthSuccess, subscribeToAuthSuccess,
error: wsError, error: wsError,
updateCell: contextUpdateCell, updateCell: contextUpdateCell,
addRow: contextAddRow, addRow: contextAddRow,
deleteRow: contextDeleteRow, deleteRow: contextDeleteRow,
startEditing: contextStartEditing,
endEditing: contextEndEditing,
} = useRealtime(); } = useRealtime();
const { const {
@ -144,6 +146,14 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
scrollPaddingStart: 0, scrollPaddingStart: 0,
scrollPaddingEnd: 0, scrollPaddingEnd: 0,
}, },
onEditingCellChange: (cell) => {
console.log(cell, 'CELL')
if (cell) {
contextStartEditing?.(cell.row, cell.column);
} else {
contextEndEditing?.(editingCell.row, editingCell.column);
}
},
columnVirtualizerOptions: ({ table }) => ({ columnVirtualizerOptions: ({ table }) => ({
overscan: 5, overscan: 5,
measureElement: (el) => { measureElement: (el) => {
@ -163,6 +173,7 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
globalFilter, globalFilter,
showColumnFilters, showColumnFilters,
rowSelection, rowSelection,
editingCell,
}, },
initialState: { initialState: {
expanded: true, expanded: true,
@ -214,6 +225,27 @@ const RealtimeTable = ({ formType, formId, sheetName, direction }) => {
const table = useMaterialReactTable(tableConfig); const table = useMaterialReactTable(tableConfig);
useEffect(() => {
if (!dataEditingCells.line_id) {
setEditingCell(null);
return;
}
if (editingCell) return;
const rowId = dataEditingCells.line_id;
const columnId = dataEditingCells.column;
try {
const row = table.getRow(rowId);
const column = table.getColumn(columnId);
const cell = row?.getVisibleCells().find(c => c.column.id === columnId);
if (cell) {
setEditingCell(cell);
}
} catch (error) {
console.error(error);
toast.error('Ошибка редактирования ячейки');
}
}, [dataEditingCells, table]);
useEffect(() => { useEffect(() => {
if (table && data && columns.length !== 0) { if (table && data && columns.length !== 0) {
setIsLoadingData(true) setIsLoadingData(true)

View File

@ -28,7 +28,8 @@ export const RealtimeProvider = ({
userId, userId,
}) => { }) => {
// Формируем 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}` : ""}`; console.log() 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 handleMessage = useCallback((data) => { const handleMessage = useCallback((data) => {
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
@ -38,8 +39,21 @@ export const RealtimeProvider = ({
} }
switch (data.event) { switch (data.event) {
case "cell_edit_start":
console.log("Cell edit started:", data);
if (onCellEditStartRef.current) {
onCellEditStartRef.current(data);
}
break;
case "cell_edit_end":
console.log("Cell edit ended:", data);
if ( onCellEditEndRef.current) {
onCellEditEndRef.current(data);
}
break;
case "cell_updated": case "cell_updated":
console.log(onCellUpdateRef.current);
if (data.result && onCellUpdateRef.current) { if (data.result && onCellUpdateRef.current) {
const updatedCells = Array.isArray(data.result) const updatedCells = Array.isArray(data.result)
? data.result ? data.result
@ -82,6 +96,8 @@ export const RealtimeProvider = ({
const onRowDeleteRef = useRef(null); const onRowDeleteRef = useRef(null);
const onErrorRef = useRef(null); const onErrorRef = useRef(null);
const onAuthSuccessRef = useRef(null); const onAuthSuccessRef = useRef(null);
const onCellEditStartRef = useRef(null);
const onCellEditEndRef = useRef(null);
const authAttemptedRef = useRef(false); const authAttemptedRef = useRef(false);
const reconnectTimerRef = useRef(null); const reconnectTimerRef = useRef(null);
@ -134,6 +150,70 @@ export const RealtimeProvider = ({
} }
}, [isConnected]); }, [isConnected]);
// Функция для начала редактирования ячейки
const startEditing = useCallback(
async (row, column) => {
const columnId = column.id;
const rowId = row.id;
if (!isConnected) {
onErrorRef.current?.("WebSocket не подключен");
return false;
}
const lineId = row.id || null;
const colId = columnId.slice(5); // убираем префикс "data."
const message = {
event: "cell_edit_start",
data: {
line_id: lineId,
column: colId,
},
};
const sent = sendMessage(message);
if (!sent) {
return false;
}
return true;
},
[isConnected, sendMessage],
);
// Функция для завершения редактирования ячейки
const endEditing = useCallback(
async (row, column) => {
const columnId = column.id;
const rowId = row.id;
if (!isConnected) {
onErrorRef.current?.("WebSocket не подключен");
return false;
}
const lineId = row.id || null;
const colId = columnId.slice(5);
const message = {
event: "cell_edit_end",
data: {
line_id: lineId,
column: colId,
},
};
const sent = sendMessage(message);
if (!sent) {
return false;
}
return true;
},
[isConnected, sendMessage],
);
const updateCell = useCallback( const updateCell = useCallback(
async (row, column, value) => { async (row, column, value) => {
const columnId = column.id; const columnId = column.id;
@ -180,7 +260,7 @@ export const RealtimeProvider = ({
}; };
return sendMessage(message); return sendMessage(message);
}, },
[isConnected, sendMessage], [isConnected, sendMessage, sheetName, formId, direction],
); );
const deleteRow = useCallback( const deleteRow = useCallback(
@ -200,7 +280,7 @@ export const RealtimeProvider = ({
return sendMessage(message); return sendMessage(message);
}, },
[isConnected, sendMessage], [isConnected, sendMessage, sheetName, formId, direction],
); );
const subscribeToCellUpdates = useCallback((callback) => { const subscribeToCellUpdates = useCallback((callback) => {
@ -238,6 +318,21 @@ export const RealtimeProvider = ({
}; };
}, []); }, []);
const subscribeToCellEditStart = useCallback((callback) => {
console.log('edit')
onCellEditStartRef.current = callback;
return () => {
onCellEditStartRef.current = null;
};
}, []);
const subscribeToCellEditEnd = useCallback((callback) => {
onCellEditEndRef.current = callback;
return () => {
onCellEditEndRef.current = null;
};
}, []);
const retryLogin = useCallback(() => { const retryLogin = useCallback(() => {
if (isConnected) { if (isConnected) {
authAttemptedRef.current = false; authAttemptedRef.current = false;
@ -250,6 +345,8 @@ export const RealtimeProvider = ({
value={{ value={{
isConnected, isConnected,
error: lastError, error: lastError,
startEditing,
endEditing,
updateCell, updateCell,
addRow, addRow,
deleteRow, deleteRow,
@ -262,6 +359,8 @@ export const RealtimeProvider = ({
subscribeToRowDeletes, subscribeToRowDeletes,
subscribeToErrors, subscribeToErrors,
subscribeToAuthSuccess, subscribeToAuthSuccess,
subscribeToCellEditStart,
subscribeToCellEditEnd,
retryLogin, retryLogin,
releaseAllLocks: () => releaseAllLocks(userId), releaseAllLocks: () => releaseAllLocks(userId),
}} }}
@ -269,4 +368,4 @@ export const RealtimeProvider = ({
{children} {children}
</RealtimeContext.Provider> </RealtimeContext.Provider>
); );
}; };

View File

@ -8,6 +8,7 @@ const useRealtimeData = (formId, sheetName, direction) => {
const [isConnected, setIsConnected] = useState(false); const [isConnected, setIsConnected] = useState(false);
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const [editingCells, setEditingCells] = useState({});
const dataRef = useRef(data); const dataRef = useRef(data);
const { const {
@ -17,9 +18,10 @@ const useRealtimeData = (formId, sheetName, direction) => {
subscribeToRowAdds, subscribeToRowAdds,
subscribeToRowDeletes, subscribeToRowDeletes,
subscribeToErrors, subscribeToErrors,
subscribeToCellEditStart,
subscribeToCellEditEnd,
} = useRealtime(); } = useRealtime();
// Ваша существующая функция форматирования
const formatedToSubRows = useCallback((depth, oldData) => { const formatedToSubRows = useCallback((depth, oldData) => {
if (depth === 0) return oldData; if (depth === 0) return oldData;
@ -204,6 +206,100 @@ 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)
// const { line_id, column } = data.data;
// 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}`);
});
// Обработчик завершения редактирования ячейки
const unsubscribeCellEditEnd = subscribeToCellEditEnd((data) => {
setEditingCells(null);
const { line_id, column } = data.data;
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`);
});
const unsubscribeErrors = subscribeToErrors((err) => { const unsubscribeErrors = subscribeToErrors((err) => {
console.error("WebSocket error:", err); console.error("WebSocket error:", err);
setError(err); setError(err);
@ -215,6 +311,8 @@ const useRealtimeData = (formId, sheetName, direction) => {
unsubscribeRowAdds(); unsubscribeRowAdds();
unsubscribeRowDeletes(); unsubscribeRowDeletes();
unsubscribeErrors(); unsubscribeErrors();
unsubscribeCellEditStart();
unsubscribeCellEditEnd();
}; };
}, [ }, [
wsConnected, wsConnected,
@ -222,6 +320,8 @@ const useRealtimeData = (formId, sheetName, direction) => {
subscribeToRowAdds, subscribeToRowAdds,
subscribeToRowDeletes, subscribeToRowDeletes,
subscribeToErrors, subscribeToErrors,
subscribeToCellEditStart,
subscribeToCellEditEnd,
formatedToSubRows, formatedToSubRows,
]); ]);
@ -244,13 +344,33 @@ 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,
// setData: updateData,
isConnected, isConnected,
isLoading, isLoading,
error, error,
editingCells,
isCellEditing,
getCellEditingInfo,
}; };
}; };

View File

@ -97,22 +97,13 @@ export const getTableColumns = ({
EditCell, EditCell,
columnsConfig, columnsConfig,
onCellUpdateError, onCellUpdateError,
onCellNumberClick onCellNumberClick,
}) => { }) => {
const columnColors = { ...columnsConfig.colors }; const columnColors = { ...columnsConfig.colors };
const columns = [...columnsConfig.columns]; const columns = [...columnsConfig.columns];
const processColumns = (columns, EditCell, Cell) => { const processColumns = (columns, EditCell, Cell) => {
columns.forEach((col) => { columns.forEach((col) => {
if (col.header.length > 15) {
col.size = 250;
}
if(col.header.length >= 20){
col.size = 350;
}
if(col.header.length >= 30){
col.size = 400;
}
col.Cell = ({ cell, table, column, row }) => ( col.Cell = ({ cell, table, column, row }) => (
<Cell <Cell
row={row} row={row}