ws для ячеек
This commit is contained in:
parent
079e719f71
commit
52de655149
@ -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,
|
||||||
@ -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}
|
||||||
|
|||||||
@ -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 },
|
||||||
@ -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,
|
|
||||||
],
|
],
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|||||||
@ -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([]);
|
||||||
@ -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;
|
||||||
);
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Находим всех потомков для текущей строки
|
||||||
|
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;
|
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) => {
|
const maxDepth = data.reduce(
|
||||||
|
(prev, current) => {
|
||||||
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
|
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
|
||||||
}, { depth: 0 }).depth;
|
},
|
||||||
|
{ depth: 0 },
|
||||||
|
).depth;
|
||||||
|
|
||||||
const formattedData = formatedToSubRows(maxDepth, mockData);
|
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,59 +118,19 @@ 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) };
|
||||||
}
|
}
|
||||||
@ -152,47 +143,49 @@ const useRealtimeData = (formId, sheetName) => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
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) {
|
|
||||||
const { subRows, ...rest } = item;
|
|
||||||
result.push(rest);
|
|
||||||
if (subRows && subRows.length > 0) {
|
|
||||||
flattenHierarchy(subRows, result);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return result;
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
const updateData = useCallback((newData) => {
|
|
||||||
if (Array.isArray(newData) && !newData[0]?.subRows) {
|
if (Array.isArray(newData) && !newData[0]?.subRows) {
|
||||||
const maxDepth = newData.reduce((prev, current) => {
|
const maxDepth = newData.reduce(
|
||||||
|
(prev, current) => {
|
||||||
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
|
return (prev.depth || 0) > (current.depth || 0) ? prev : current;
|
||||||
}, { depth: 0 }).depth;
|
},
|
||||||
|
{ depth: 0 },
|
||||||
|
).depth;
|
||||||
|
|
||||||
const formattedData = formatedToSubRows(maxDepth, newData);
|
const formattedData = formatedToSubRows(maxDepth, newData);
|
||||||
setData(formattedData);
|
setData(formattedData);
|
||||||
} else {
|
} else {
|
||||||
setData(newData);
|
setData(newData);
|
||||||
}
|
}
|
||||||
}, [formatedToSubRows]);
|
},
|
||||||
|
[formatedToSubRows],
|
||||||
|
);
|
||||||
|
|
||||||
return {
|
return {
|
||||||
data,
|
data,
|
||||||
setData: updateData,
|
setData,
|
||||||
|
// setData: updateData,
|
||||||
isConnected,
|
isConnected,
|
||||||
isLoading,
|
isLoading,
|
||||||
error,
|
error,
|
||||||
|
|||||||
@ -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
|
|
||||||
};
|
|
||||||
};
|
|
||||||
@ -7,7 +7,6 @@ function EditCellPortal({
|
|||||||
cell,
|
cell,
|
||||||
table,
|
table,
|
||||||
EditCell,
|
EditCell,
|
||||||
onChange,
|
|
||||||
onSaveStart,
|
onSaveStart,
|
||||||
onSaveEnd,
|
onSaveEnd,
|
||||||
onError
|
onError
|
||||||
@ -25,6 +24,7 @@ function EditCellPortal({
|
|||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleChange = async (val) => {
|
const handleChange = async (val) => {
|
||||||
|
table.setEditingCell(null);
|
||||||
if (isSaving) return;
|
if (isSaving) return;
|
||||||
|
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
@ -32,20 +32,16 @@ function EditCellPortal({
|
|||||||
|
|
||||||
// Сохраняем старое значение на случай ошибки
|
// Сохраняем старое значение на случай ошибки
|
||||||
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);
|
||||||
}
|
}
|
||||||
@ -122,7 +117,8 @@ 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];
|
||||||
@ -148,12 +144,6 @@ export const getTableColumns = ({
|
|||||||
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>;
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
94
web/src/components/RealtimeTable/utils/cellUtils.js
Normal file
94
web/src/components/RealtimeTable/utils/cellUtils.js
Normal 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;
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user