ws для ячеек

This commit is contained in:
PotapovaA 2026-05-28 12:12:16 +03:00
parent 079e719f71
commit 52de655149
6 changed files with 242 additions and 432 deletions

View File

@ -21,6 +21,7 @@ import {
getTablePaperStyles, getTablePaperStyles,
} from './constants/tableConfig'; } from './constants/tableConfig';
import { useRealtime } from './contexts/RealtimeContext'; import { useRealtime } from './contexts/RealtimeContext';
import { setNewValueToData, updateExistingStructure } from './utils/cellUtils';
const RealtimeTable = ({ formType, formId, sheetName }) => { const RealtimeTable = ({ formType, formId, sheetName }) => {
const { data, setData } = useRealtimeData(formId, sheetName); const { data, setData } = useRealtimeData(formId, sheetName);
@ -28,9 +29,8 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
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 [updatingCells, setUpdatingCells] = useState({});
const columnVirtualizerInstanceRef = useRef(null); const columnVirtualizerInstanceRef = useRef(null);
const [rowSelection, setRowSelection] = useState({});
const { const {
isConnected, isConnected,
@ -39,7 +39,7 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
subscribeToErrors, subscribeToErrors,
subscribeToAuthSuccess, subscribeToAuthSuccess,
error: wsError, error: wsError,
updateCell: contextUpdateCell updateCell: contextUpdateCell
} = useRealtime(); } = useRealtime();
const { const {
@ -75,75 +75,16 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
loadConfig(); loadConfig();
}, [formType, sheetName]); }, [formType, sheetName]);
// Подписка на обновления от других пользователей const handleUpdateCell = useCallback(async (row, column, value) => {
useEffect(() => { return await contextUpdateCell(row, column, value);
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
};
}
return newData;
});
// Отправляем через WebSocket
return await contextUpdateCell(rowId, columnId, value);
}, [contextUpdateCell, setData]); }, [contextUpdateCell, setData]);
const handleClickRowCell = useCallback((rowId) => {
setRowSelection((prev) => ({
[rowId]: !prev[rowId],
}));
}, [rowSelection])
// Создание колонок с интеграцией WebSocket // Создание колонок с интеграцией WebSocket
const columns = useMemo(() => { const columns = useMemo(() => {
if (!columnsConfig || !columnsConfig.columns) return []; if (!columnsConfig || !columnsConfig.columns) return [];
@ -152,10 +93,11 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
Cell, Cell,
EditCell, EditCell,
columnsConfig, columnsConfig,
onCellUpdate: handleUpdateCell, // Передаем функцию обновления onCellUpdate: handleUpdateCell,
onCellNumberClick: handleClickRowCell,
onCellUpdateError: (rowId, columnId, error) => { onCellUpdateError: (rowId, columnId, error) => {
console.error(`Error updating cell ${rowId}_${columnId}:`, error); console.error(`Error updating cell ${rowId}_${columnId}:`, error);
// Можно добавить Toast уведомление toast.error('Ошибка обновления ячейки')
} }
}); });
} catch (error) { } catch (error) {
@ -166,19 +108,19 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
// Метаданные для таблицы // Метаданные для таблицы
const tableMeta = useMemo(() => ({ const tableMeta = useMemo(() => ({
updatingCells,
setUpdatingCell,
updateCell: handleUpdateCell, updateCell: handleUpdateCell,
}), [updatingCells, setUpdatingCell]); }), [handleUpdateCell]);
// Конфигурация таблицы // Конфигурация таблицы
const tableConfig = useMemo( const tableConfig = useMemo(
() => ({ () => ({
...BASE_TABLE_CONFIG, ...BASE_TABLE_CONFIG,
columns, columns,
data: data || [], data: data,
columnVirtualizerInstanceRef, columnVirtualizerInstanceRef,
enableRowVirtualization: false, enableRowVirtualization: false,
enableRowSelection: true,
onRowSelectionChange: setRowSelection,
onColumnSizingChange: setColumnSizing, onColumnSizingChange: setColumnSizing,
onColumnPinningChange: setColumnPinning, onColumnPinningChange: setColumnPinning,
onGlobalFilterChange: setGlobalFilter, onGlobalFilterChange: setGlobalFilter,
@ -188,6 +130,10 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
columnVisibility, columnVisibility,
globalFilter, globalFilter,
showColumnFilters, showColumnFilters,
rowSelection
},
initialState: {
hiddenColumn: []
}, },
meta: tableMeta, meta: tableMeta,
muiTableHeadCellProps: ({ column }) => ({ muiTableHeadCellProps: ({ column }) => ({
@ -244,47 +190,18 @@ const RealtimeTable = ({ formType, formId, sheetName }) => {
showColumnFilters, showColumnFilters,
containerRef, containerRef,
tableMeta, tableMeta,
rowSelection,
], ],
); );
const table = useMaterialReactTable(tableConfig); const table = useMaterialReactTable(tableConfig);
// Индикатор подключения 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 (!data) { if (!data) {
return <div>Loading...</div>; return <div>Loading...</div>;
} }
return ( return (
<> <>
<ConnectionStatus />
<SettingsPanel <SettingsPanel
selectedColumn={selectedColumn} selectedColumn={selectedColumn}
table={table} table={table}

View File

@ -8,6 +8,7 @@ import React, {
} from "react"; } from "react";
import { useWebSocket } from "../hooks/useWebSocket"; import { useWebSocket } from "../hooks/useWebSocket";
import { useCellLocks } from "../hooks/useCellLocks"; import { useCellLocks } from "../hooks/useCellLocks";
import { toast } from "react-toastify";
const RealtimeContext = createContext(null); const RealtimeContext = createContext(null);
@ -26,18 +27,13 @@ export const RealtimeProvider = ({
direction, direction,
userId, userId,
}) => { }) => {
console.log(children, formId, sheetName, direction, userId);
const [pendingUpdates, setPendingUpdates] = useState(new Map());
// Формируем URL // Формируем URL
const wsUrl = `${process.env.REACT_APP_WS_URL || "ws://localhost:8000"}/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ""}`; 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) => { const handleMessage = useCallback((data) => {
console.log("Received message:", data);
// Обработка ошибок // Обработка ошибок
if (data.error) { if (data.error) {
console.error("Server error:", data.error); toast.error("Server error:", data.error)
onErrorRef.current?.(data.error); onErrorRef.current?.(data.error);
return; return;
} }
@ -45,6 +41,7 @@ export const RealtimeProvider = ({
// Обработка событий // Обработка событий
switch (data.event) { switch (data.event) {
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
@ -115,7 +112,6 @@ export const RealtimeProvider = ({
return false; return false;
} }
console.log("Sending login message with token");
const loginMessage = { const loginMessage = {
event: "user_login", event: "user_login",
data: { token }, data: { token },
@ -132,7 +128,7 @@ export const RealtimeProvider = ({
sendLogin(); sendLogin();
}, 100); }, 100);
} }
}, [isConnected, sendLogin]); }, [isConnected, sendLogin]);
useEffect(() => { useEffect(() => {
if (!isConnected) { if (!isConnected) {
@ -141,43 +137,29 @@ export const RealtimeProvider = ({
}, [isConnected]); }, [isConnected]);
const updateCell = useCallback( const updateCell = useCallback(
async (rowId, columnId, value) => { async (row, column, value) => {
const columnId = column.id;
const rowId = row.id;
if (!isConnected) { if (!isConnected) {
onErrorRef.current?.("WebSocket не подключен"); onErrorRef.current?.("WebSocket не подключен");
return false; return false;
} }
if (!tryLockCell(rowId, columnId, userId)) {
const lockInfo = getCellLockInfo(rowId, columnId);
onErrorRef.current?.(
`Ячейка редактируется пользователем ${lockInfo?.userId}`,
);
return false;
}
const lineId = row.original?.data?.line_id || null;
const colId = columnId.slice(5);
const message = { const message = {
event: "cell_updated", event: "cell_updated",
data: { data: {
line_id: rowId, line_id: lineId,
column: columnId, column: colId,
value: value, value: value,
}, },
}; };
console.log(message);
const updateKey = `${rowId}_${columnId}`;
setPendingUpdates((prev) =>
new Map(prev).set(updateKey, { value, timestamp: Date.now() }),
);
const sent = sendMessage(message); const sent = sendMessage(message);
if (!sent) { if (!sent) {
unlockCell(rowId, columnId, userId);
setPendingUpdates((prev) => {
const newMap = new Map(prev);
newMap.delete(updateKey);
return newMap;
});
return false; return false;
} }
@ -186,10 +168,6 @@ export const RealtimeProvider = ({
[ [
isConnected, isConnected,
sendMessage, sendMessage,
tryLockCell,
unlockCell,
getCellLockInfo,
userId,
], ],
); );

View File

@ -1,6 +1,7 @@
import { useState, useEffect, useCallback, useRef } from "react"; import { useState, useEffect, useCallback, useRef } from "react";
import { FormsSheetApi } from "../../../api/form_sheet"; import { FormsSheetApi } from "../../../api/form_sheet";
import { useRealtime } from "../contexts/RealtimeContext"; import { useRealtime } from "../contexts/RealtimeContext";
import { updateExistingStructure } from "../utils/cellUtils";
const useRealtimeData = (formId, sheetName) => { const useRealtimeData = (formId, sheetName) => {
const [data, setData] = useState([]); const [data, setData] = useState([]);
@ -8,7 +9,7 @@ const useRealtimeData = (formId, sheetName) => {
const [error, setError] = useState(null); const [error, setError] = useState(null);
const [isLoading, setIsLoading] = useState(true); const [isLoading, setIsLoading] = useState(true);
const dataRef = useRef(data); const dataRef = useRef(data);
const { const {
isConnected: wsConnected, isConnected: wsConnected,
error: wsError, error: wsError,
@ -21,21 +22,48 @@ const useRealtimeData = (formId, sheetName) => {
// Ваша существующая функция форматирования // Ваша существующая функция форматирования
const formatedToSubRows = useCallback((depth, oldData) => { const formatedToSubRows = useCallback((depth, oldData) => {
if (depth === 0) return oldData; if (depth === 0) return oldData;
const dataFiltered = oldData.filter((d) => d.depth < depth); const dataFiltered = oldData.filter((d) => d.depth < depth);
for (let i = 0; i < dataFiltered.length; i++) { for (let i = 0; i < dataFiltered.length; i++) {
const curRow = dataFiltered[i]; const curRow = dataFiltered[i];
if (curRow.depth !== depth - 1) continue;
const nextRow = dataFiltered[i + 1] || null; // Находим следующий элемент на том же или более высоком уровне
const subRows = oldData.filter( let nextRow = null;
(d) => for (let j = i + 1; j < dataFiltered.length; j++) {
d.depth === depth && if (dataFiltered[j].depth <= curRow.depth) {
d.sort_order > curRow.sort_order && nextRow = dataFiltered[j];
(!nextRow || d.sort_order < nextRow.sort_order), break;
); }
curRow.subRows = subRows; }
// Находим всех потомков для текущей строки
const subRows = oldData.filter((d) => {
// Потомок должен иметь глубину больше чем у родителя
if (d.depth <= curRow.depth) return false;
// Потомок должен идти после родителя по sort_order
if (d.sort_order <= curRow.sort_order) return false;
// Если есть следующий элемент на том же уровне - потомок должен быть до него
if (nextRow && d.sort_order >= nextRow.sort_order) return false;
return true;
});
if (subRows.length > 0) {
curRow.subRows = subRows;
}
} }
return formatedToSubRows(depth - 1, dataFiltered);
// Рекурсивно обрабатываем следующий уровень
// Находим максимальную глубину в текущем dataFiltered
const maxDepth = Math.max(...dataFiltered.map((d) => d.depth));
if (maxDepth > 0) {
return formatedToSubRows(maxDepth, dataFiltered);
}
return dataFiltered;
}, []); }, []);
// Загрузка начальных данных // Загрузка начальных данных
@ -44,21 +72,24 @@ const useRealtimeData = (formId, sheetName) => {
try { try {
setIsLoading(true); setIsLoading(true);
const res = await FormsSheetApi.get(formId, sheetName); const res = await FormsSheetApi.get(formId, sheetName);
const mockData = res.result; const data = res.result;
if (!mockData || mockData.length === 0) { if (!data || data.length === 0) {
setData([]); setData([]);
return; return;
} }
const maxDepth = mockData.reduce((prev, current) => {
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
}, { depth: 0 }).depth;
const formattedData = formatedToSubRows(maxDepth, mockData); const maxDepth = data.reduce(
(prev, current) => {
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
},
{ depth: 0 },
).depth;
const formattedData = formatedToSubRows(maxDepth, data);
setData(formattedData); setData(formattedData);
} catch (err) { } catch (err) {
console.error('Failed to load initial data:', err); console.error("Failed to load initial data:", err);
setError(err.message); setError(err.message);
} finally { } finally {
setIsLoading(false); setIsLoading(false);
@ -87,116 +118,78 @@ const useRealtimeData = (formId, sheetName) => {
const unsubscribeCellUpdates = subscribeToCellUpdates((updatedCells) => { const unsubscribeCellUpdates = subscribeToCellUpdates((updatedCells) => {
if (!updatedCells || updatedCells.length === 0) return; if (!updatedCells || updatedCells.length === 0) return;
setData(prevData => { setData((prevData) => {
// Функция для обновления данных в иерархической структуре const newData = updateExistingStructure(prevData, updatedCells)
const updateInHierarchy = (items) => { return newData
return items.map(item => {
let updatedItem = { ...item };
// Обновляем ячейки текущего уровня
updatedCells.forEach(cell => {
if ((item.id || item.line_id) === cell.line_id) {
updatedItem[cell.column] = cell.value;
}
});
// Рекурсивно обновляем подстроки
if (item.subRows && item.subRows.length > 0) {
updatedItem.subRows = updateInHierarchy(item.subRows);
}
return updatedItem;
});
};
return updateInHierarchy(prevData);
});
});
const unsubscribeRowAdds = subscribeToRowAdds((newRow) => {
setData(prevData => {
// Преобразуем текущие данные в плоский массив
const flatData = flattenHierarchy(prevData);
// Добавляем новую строку
const updatedFlatData = [...flatData, newRow];
// Сортируем по sort_order
updatedFlatData.sort((a, b) => (a.sort_order || 0) - (b.sort_order || 0));
// Восстанавливаем иерархию
const maxDepth = updatedFlatData.reduce((prev, current) => {
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
}, { depth: 0 }).depth;
return formatedToSubRows(maxDepth, updatedFlatData);
}); });
}); });
const unsubscribeRowDeletes = subscribeToRowDeletes((deletedRowId) => { const unsubscribeRowDeletes = subscribeToRowDeletes((deletedRowId) => {
setData(prevData => { setData((prevData) => {
// Функция для удаления строки из иерархии // Функция для удаления строки из иерархии
const deleteInHierarchy = (items) => { const deleteInHierarchy = (items) => {
return items return items
.filter(item => (item.id || item.line_id) !== deletedRowId) .filter((item) => (item.id || item.line_id) !== deletedRowId)
.map(item => { .map((item) => {
if (item.subRows && item.subRows.length > 0) { if (item.subRows && item.subRows.length > 0) {
return { ...item, subRows: deleteInHierarchy(item.subRows) }; return { ...item, subRows: deleteInHierarchy(item.subRows) };
} }
return item; return item;
}); });
}; };
return deleteInHierarchy(prevData); return deleteInHierarchy(prevData);
}); });
}); });
const unsubscribeErrors = subscribeToErrors((err) => { const unsubscribeErrors = subscribeToErrors((err) => {
console.error('WebSocket error:', err); console.error("WebSocket error:", err);
setError(err); setError(err);
setTimeout(() => setError(null), 5000); setTimeout(() => setError(null), 5000);
}); });
return () => { return () => {
unsubscribeCellUpdates(); unsubscribeCellUpdates();
unsubscribeRowAdds(); // unsubscribeRowAdds();
unsubscribeRowDeletes(); unsubscribeRowDeletes();
unsubscribeErrors(); unsubscribeErrors();
}; };
}, [wsConnected, subscribeToCellUpdates, subscribeToRowAdds, subscribeToRowDeletes, subscribeToErrors, formatedToSubRows]); }, [
wsConnected,
subscribeToCellUpdates,
subscribeToRowAdds,
subscribeToRowDeletes,
subscribeToErrors,
formatedToSubRows,
]);
// Вспомогательная функция для преобразования иерархических данных в плоский массив const updateData = useCallback(
const flattenHierarchy = useCallback((items, result = []) => { (newData) => {
for (const item of items) { if (Array.isArray(newData) && !newData[0]?.subRows) {
const { subRows, ...rest } = item; const maxDepth = newData.reduce(
result.push(rest); (prev, current) => {
if (subRows && subRows.length > 0) { return (prev.depth || 0) > (current.depth || 0) ? prev : current;
flattenHierarchy(subRows, result); },
{ depth: 0 },
).depth;
const formattedData = formatedToSubRows(maxDepth, newData);
setData(formattedData);
} else {
setData(newData);
} }
} },
return result; [formatedToSubRows],
}, []); );
const updateData = useCallback((newData) => {
if (Array.isArray(newData) && !newData[0]?.subRows) {
const maxDepth = newData.reduce((prev, current) => {
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
}, { depth: 0 }).depth;
const formattedData = formatedToSubRows(maxDepth, newData);
setData(formattedData);
} else {
setData(newData);
}
}, [formatedToSubRows]);
return { return {
data, data,
setData: updateData, setData,
// setData: updateData,
isConnected, isConnected,
isLoading, isLoading,
error, error,
}; };
}; };
export default useRealtimeData; export default useRealtimeData;

View File

@ -1,165 +0,0 @@
// 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
};
};

View File

@ -3,14 +3,13 @@ import { createPortal } from 'react-dom';
import { columns as mockColumns } from '../../mocks/mockTable'; import { columns as mockColumns } from '../../mocks/mockTable';
import { config } from './constants/FORM_2/AHR'; import { config } from './constants/FORM_2/AHR';
function EditCellPortal({ function EditCellPortal({
cell, cell,
table, table,
EditCell, EditCell,
onChange,
onSaveStart, onSaveStart,
onSaveEnd, onSaveEnd,
onError onError
}) { }) {
const ref = useRef(null); const ref = useRef(null);
const [refTbody, setRefTbody] = useState(false); const [refTbody, setRefTbody] = useState(false);
@ -25,27 +24,24 @@ function EditCellPortal({
}, []); }, []);
const handleChange = async (val) => { const handleChange = async (val) => {
table.setEditingCell(null);
if (isSaving) return; if (isSaving) return;
setIsSaving(true); setIsSaving(true);
onSaveStart?.(); onSaveStart?.();
// Сохраняем старое значение на случай ошибки // Сохраняем старое значение на случай ошибки
const oldValue = cell.getValue(); const oldValue = cell.getValue();
console.log(table.options.meta)
try { try {
// Сначала обновляем локальное состояние (оптимистичное обновление)
onChange(val);
// Используем updateCell из контекста (передается через meta) // Используем updateCell из контекста (передается через meta)
if (table.options.meta?.updateCell) { if (table.options.meta?.updateCell) {
const success = await table.options.meta.updateCell( const success = await table.options.meta.updateCell(
cell.row.id, cell.row,
cell.column.id, cell.column,
val val
); );
if (success) { if (success) {
console.log('Cell updated successfully via WebSocket'); console.log('Cell updated successfully via WebSocket');
onSaveEnd?.(true); onSaveEnd?.(true);
@ -56,7 +52,7 @@ function EditCellPortal({
onError?.(errorMsg); onError?.(errorMsg);
onSaveEnd?.(false); onSaveEnd?.(false);
// Возвращаем предыдущее значение // Возвращаем предыдущее значение
onChange(oldValue);
} }
} else { } else {
// Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback) // Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback)
@ -71,8 +67,7 @@ function EditCellPortal({
console.error('Error updating cell:', error); console.error('Error updating cell:', error);
onError?.(error.message); onError?.(error.message);
onSaveEnd?.(false); onSaveEnd?.(false);
// Возвращаем предыдущее значение
onChange(oldValue);
} finally { } finally {
setIsSaving(false); setIsSaving(false);
} }
@ -118,11 +113,12 @@ function EditCellPortal({
); );
} }
export const getTableColumns = ({ export const getTableColumns = ({
Cell, Cell,
EditCell, EditCell,
columnsConfig, columnsConfig,
onCellUpdateError onCellUpdateError,
onCellNumberClick
}) => { }) => {
const columnColors = { ...columnsConfig.colors }; const columnColors = { ...columnsConfig.colors };
const columns = [...columnsConfig.columns]; const columns = [...columnsConfig.columns];
@ -142,18 +138,12 @@ export const getTableColumns = ({
isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]} isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]}
/> />
); );
col.Edit = ({ cell, table, row, column }) => ( col.Edit = ({ cell, table, row, column }) => (
<EditCellPortal <EditCellPortal
cell={cell} cell={cell}
table={table} table={table}
EditCell={EditCell} EditCell={EditCell}
onChange={(value) => {
// Локальное обновление кэша
if (row._valuesCache) {
row._valuesCache[column.id] = value;
}
}}
onSaveStart={() => { onSaveStart={() => {
if (table.options.meta?.setUpdatingCell) { if (table.options.meta?.setUpdatingCell) {
table.options.meta.setUpdatingCell(row.id, column.id, true); table.options.meta.setUpdatingCell(row.id, column.id, true);
@ -186,8 +176,11 @@ export const getTableColumns = ({
size: 70, size: 70,
enableEditing: false, enableEditing: false,
enablePinning: true, enablePinning: true,
Cell: ({ cell }) => { Cell: ({ cell, row }) => {
return <span>{cell.getValue()}</span>; return <span
onClick={() => {console.log('TEST', row.id); onCellNumberClick(row.id)}}
>{cell.getValue()}</span>;
}, },
}; };

View File

@ -0,0 +1,94 @@
export function setNewValueToData(data, colId, rowId, value) {
// 1. Находим целевой объект по rowId(индексы вложенных subRows)
let targetObject = data;
if (rowId && rowId.length) {
const indices = rowId.split(".").map(Number);
for (let i = 0; i < indices.length; i++) {
const index = indices[i];
if (targetObject && targetObject[index] !== undefined) {
targetObject = targetObject[index];
} else {
console.error(`Индекс ${index} не найден на уровне ${i}`);
return false;
}
// Если это не последний индекс, переходим в subRows
if (i < indices.length - 1) {
if (targetObject.subRows && Array.isArray(targetObject.subRows)) {
targetObject = targetObject.subRows;
} else {
console.error(`Нет subRows для перехода на уровне ${i}`);
return false;
}
}
}
}
const keys = colId.split(".");
const lastKey = keys.pop();
// Идем по вложенности path
let current = targetObject;
for (let i = 0; i < keys.length; i++) {
const key = keys[i];
if (!current[key]) {
current[key] = {};
}
current = current[key];
}
current[lastKey] = value;
return true;
}
export function updateExistingStructure(existingRows, updateItems) {
const updatedRows = JSON.parse(JSON.stringify(existingRows));
updateItems.forEach((update) => {
const [rowType, depth, sortOrder, newData] = update;
// Рекурсивная функция для поиска и обновления строки
function findAndUpdate(rows) {
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
// Проверяем совпадение по всем ключам
if (
row.row_type === rowType &&
row.depth === depth &&
row.sort_order === sortOrder
) {
// Обновляем только те поля, которые пришли в newData
if (newData && typeof newData === "object") {
Object.keys(newData).forEach((key) => {
if (row.data && row.data[key]) {
// Если поле существует в data, обновляем его
row.data[key] = {...row.data[key], ...newData[key]};
} else if (row[key]) {
// Если поле на уровне самой строки (не в data)
row[key] = newData[key];
}
});
}
return true; // Нашли и обновили
}
// Рекурсивно ищем в subRows
if (
row.subRows &&
Array.isArray(row.subRows) &&
row.subRows.length > 0
) {
const found = findAndUpdate(row.subRows);
if (found) return true;
}
}
return false;
}
findAndUpdate(updatedRows);
});
return updatedRows;
}