diff --git a/web/src/components/RealtimeTable/RealtimeTable.jsx b/web/src/components/RealtimeTable/RealtimeTable.jsx index d322b4b..063dbd3 100644 --- a/web/src/components/RealtimeTable/RealtimeTable.jsx +++ b/web/src/components/RealtimeTable/RealtimeTable.jsx @@ -21,6 +21,7 @@ import { getTablePaperStyles, } from './constants/tableConfig'; import { useRealtime } from './contexts/RealtimeContext'; +import { setNewValueToData, updateExistingStructure } from './utils/cellUtils'; const RealtimeTable = ({ formType, formId, sheetName }) => { const { data, setData } = useRealtimeData(formId, sheetName); @@ -28,9 +29,8 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { const [globalFilter, setGlobalFilter] = useState(''); const [showColumnFilters, setShowColumnFilters] = useState(false); const [selectedColumn, setSelectedColumn] = useState(); - const [updatingCells, setUpdatingCells] = useState({}); - const columnVirtualizerInstanceRef = useRef(null); + const [rowSelection, setRowSelection] = useState({}); const { isConnected, @@ -39,7 +39,7 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { subscribeToErrors, subscribeToAuthSuccess, error: wsError, - updateCell: contextUpdateCell + updateCell: contextUpdateCell } = useRealtime(); const { @@ -75,75 +75,16 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { loadConfig(); }, [formType, sheetName]); - // Подписка на обновления от других пользователей - useEffect(() => { - 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); + const handleUpdateCell = useCallback(async (row, column, value) => { + return await contextUpdateCell(row, column, value); }, [contextUpdateCell, setData]); + const handleClickRowCell = useCallback((rowId) => { + setRowSelection((prev) => ({ + [rowId]: !prev[rowId], + })); + }, [rowSelection]) + // Создание колонок с интеграцией WebSocket const columns = useMemo(() => { if (!columnsConfig || !columnsConfig.columns) return []; @@ -152,10 +93,11 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { Cell, EditCell, columnsConfig, - onCellUpdate: handleUpdateCell, // Передаем функцию обновления + onCellUpdate: handleUpdateCell, + onCellNumberClick: handleClickRowCell, onCellUpdateError: (rowId, columnId, error) => { console.error(`Error updating cell ${rowId}_${columnId}:`, error); - // Можно добавить Toast уведомление + toast.error('Ошибка обновления ячейки') } }); } catch (error) { @@ -166,19 +108,19 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { // Метаданные для таблицы const tableMeta = useMemo(() => ({ - updatingCells, - setUpdatingCell, updateCell: handleUpdateCell, - }), [updatingCells, setUpdatingCell]); + }), [handleUpdateCell]); // Конфигурация таблицы const tableConfig = useMemo( () => ({ ...BASE_TABLE_CONFIG, columns, - data: data || [], + data: data, columnVirtualizerInstanceRef, enableRowVirtualization: false, + enableRowSelection: true, + onRowSelectionChange: setRowSelection, onColumnSizingChange: setColumnSizing, onColumnPinningChange: setColumnPinning, onGlobalFilterChange: setGlobalFilter, @@ -188,6 +130,10 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { columnVisibility, globalFilter, showColumnFilters, + rowSelection + }, + initialState: { + hiddenColumn: [] }, meta: tableMeta, muiTableHeadCellProps: ({ column }) => ({ @@ -244,47 +190,18 @@ const RealtimeTable = ({ formType, formId, sheetName }) => { showColumnFilters, containerRef, tableMeta, + rowSelection, ], ); const table = useMaterialReactTable(tableConfig); - // Индикатор подключения WebSocket - const ConnectionStatus = () => ( -
- {console.log(isConnected ,isAuthenticated )} - - {isConnected && isAuthenticated ? '● Connected' : '○ Disconnected'} - {wsError &&
{wsError}
} -
- ); - if (!data) { return
Loading...
; } return ( <> - { - console.log(children, formId, sheetName, direction, userId); - const [pendingUpdates, setPendingUpdates] = useState(new Map()); - // Формируем URL 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) => { - console.log("Received message:", data); - // Обработка ошибок if (data.error) { - console.error("Server error:", data.error); + toast.error("Server error:", data.error) onErrorRef.current?.(data.error); return; } @@ -45,6 +41,7 @@ export const RealtimeProvider = ({ // Обработка событий switch (data.event) { case "cell_updated": + console.log(onCellUpdateRef.current) if (data.result && onCellUpdateRef.current) { const updatedCells = Array.isArray(data.result) ? data.result @@ -115,7 +112,6 @@ export const RealtimeProvider = ({ return false; } - console.log("Sending login message with token"); const loginMessage = { event: "user_login", data: { token }, @@ -132,7 +128,7 @@ export const RealtimeProvider = ({ sendLogin(); }, 100); } - }, [isConnected, sendLogin]); + }, [isConnected, sendLogin]); useEffect(() => { if (!isConnected) { @@ -141,43 +137,29 @@ export const RealtimeProvider = ({ }, [isConnected]); const updateCell = useCallback( - async (rowId, columnId, value) => { + async (row, column, value) => { + const columnId = column.id; + const rowId = row.id; if (!isConnected) { onErrorRef.current?.("WebSocket не подключен"); 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 = { event: "cell_updated", data: { - line_id: rowId, - column: columnId, + line_id: lineId, + column: colId, value: value, }, }; - - const updateKey = `${rowId}_${columnId}`; - setPendingUpdates((prev) => - new Map(prev).set(updateKey, { value, timestamp: Date.now() }), - ); - + console.log(message); const sent = sendMessage(message); if (!sent) { - unlockCell(rowId, columnId, userId); - setPendingUpdates((prev) => { - const newMap = new Map(prev); - newMap.delete(updateKey); - return newMap; - }); return false; } @@ -186,10 +168,6 @@ export const RealtimeProvider = ({ [ isConnected, sendMessage, - tryLockCell, - unlockCell, - getCellLockInfo, - userId, ], ); diff --git a/web/src/components/RealtimeTable/hooks/useRealtimeData.js b/web/src/components/RealtimeTable/hooks/useRealtimeData.js index 319e38c..857f434 100644 --- a/web/src/components/RealtimeTable/hooks/useRealtimeData.js +++ b/web/src/components/RealtimeTable/hooks/useRealtimeData.js @@ -1,6 +1,7 @@ import { useState, useEffect, useCallback, useRef } from "react"; import { FormsSheetApi } from "../../../api/form_sheet"; import { useRealtime } from "../contexts/RealtimeContext"; +import { updateExistingStructure } from "../utils/cellUtils"; const useRealtimeData = (formId, sheetName) => { const [data, setData] = useState([]); @@ -8,7 +9,7 @@ const useRealtimeData = (formId, sheetName) => { const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(true); const dataRef = useRef(data); - + const { isConnected: wsConnected, error: wsError, @@ -21,21 +22,48 @@ const useRealtimeData = (formId, sheetName) => { // Ваша существующая функция форматирования const formatedToSubRows = useCallback((depth, oldData) => { if (depth === 0) return oldData; + const dataFiltered = oldData.filter((d) => d.depth < depth); for (let i = 0; i < dataFiltered.length; i++) { const curRow = dataFiltered[i]; - if (curRow.depth !== depth - 1) continue; - const nextRow = dataFiltered[i + 1] || null; - const subRows = oldData.filter( - (d) => - d.depth === depth && - d.sort_order > curRow.sort_order && - (!nextRow || d.sort_order < nextRow.sort_order), - ); - curRow.subRows = subRows; + + // Находим следующий элемент на том же или более высоком уровне + let nextRow = null; + for (let j = i + 1; j < dataFiltered.length; j++) { + if (dataFiltered[j].depth <= curRow.depth) { + nextRow = dataFiltered[j]; + 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; + } } - 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 { setIsLoading(true); const res = await FormsSheetApi.get(formId, sheetName); - const mockData = res.result; - - if (!mockData || mockData.length === 0) { + const data = res.result; + + if (!data || data.length === 0) { setData([]); 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); } catch (err) { - console.error('Failed to load initial data:', err); + console.error("Failed to load initial data:", err); setError(err.message); } finally { setIsLoading(false); @@ -87,116 +118,78 @@ const useRealtimeData = (formId, sheetName) => { const unsubscribeCellUpdates = subscribeToCellUpdates((updatedCells) => { if (!updatedCells || updatedCells.length === 0) return; - setData(prevData => { - // Функция для обновления данных в иерархической структуре - const updateInHierarchy = (items) => { - 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); + setData((prevData) => { + const newData = updateExistingStructure(prevData, updatedCells) + return newData }); }); const unsubscribeRowDeletes = subscribeToRowDeletes((deletedRowId) => { - setData(prevData => { + setData((prevData) => { // Функция для удаления строки из иерархии const deleteInHierarchy = (items) => { return items - .filter(item => (item.id || item.line_id) !== deletedRowId) - .map(item => { + .filter((item) => (item.id || item.line_id) !== deletedRowId) + .map((item) => { if (item.subRows && item.subRows.length > 0) { return { ...item, subRows: deleteInHierarchy(item.subRows) }; } return item; }); }; - + return deleteInHierarchy(prevData); }); }); const unsubscribeErrors = subscribeToErrors((err) => { - console.error('WebSocket error:', err); + console.error("WebSocket error:", err); setError(err); setTimeout(() => setError(null), 5000); }); return () => { unsubscribeCellUpdates(); - unsubscribeRowAdds(); + // unsubscribeRowAdds(); unsubscribeRowDeletes(); unsubscribeErrors(); }; - }, [wsConnected, subscribeToCellUpdates, subscribeToRowAdds, subscribeToRowDeletes, subscribeToErrors, formatedToSubRows]); + }, [ + wsConnected, + subscribeToCellUpdates, + subscribeToRowAdds, + subscribeToRowDeletes, + subscribeToErrors, + formatedToSubRows, + ]); - // Вспомогательная функция для преобразования иерархических данных в плоский массив - const flattenHierarchy = useCallback((items, result = []) => { - for (const item of items) { - const { subRows, ...rest } = item; - result.push(rest); - if (subRows && subRows.length > 0) { - flattenHierarchy(subRows, result); + 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); } - } - return result; - }, []); - - 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]); + }, + [formatedToSubRows], + ); return { data, - setData: updateData, + setData, + // setData: updateData, isConnected, isLoading, error, }; }; -export default useRealtimeData; \ No newline at end of file +export default useRealtimeData; diff --git a/web/src/components/RealtimeTable/hooks/useTableWebSocket.js b/web/src/components/RealtimeTable/hooks/useTableWebSocket.js deleted file mode 100644 index 8e28018..0000000 --- a/web/src/components/RealtimeTable/hooks/useTableWebSocket.js +++ /dev/null @@ -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 - }; -}; \ No newline at end of file diff --git a/web/src/components/RealtimeTable/tableColumns.jsx b/web/src/components/RealtimeTable/tableColumns.jsx index 7afd9c4..d6fc397 100644 --- a/web/src/components/RealtimeTable/tableColumns.jsx +++ b/web/src/components/RealtimeTable/tableColumns.jsx @@ -3,14 +3,13 @@ import { createPortal } from 'react-dom'; import { columns as mockColumns } from '../../mocks/mockTable'; import { config } from './constants/FORM_2/AHR'; -function EditCellPortal({ - cell, - table, - EditCell, - onChange, +function EditCellPortal({ + cell, + table, + EditCell, onSaveStart, onSaveEnd, - onError + onError }) { const ref = useRef(null); const [refTbody, setRefTbody] = useState(false); @@ -25,27 +24,24 @@ function EditCellPortal({ }, []); const handleChange = async (val) => { + table.setEditingCell(null); if (isSaving) return; - + setIsSaving(true); onSaveStart?.(); - + // Сохраняем старое значение на случай ошибки const oldValue = cell.getValue(); - console.log(table.options.meta) - + try { - // Сначала обновляем локальное состояние (оптимистичное обновление) - onChange(val); - + // Используем updateCell из контекста (передается через meta) if (table.options.meta?.updateCell) { const success = await table.options.meta.updateCell( - cell.row.id, - cell.column.id, + cell.row, + cell.column, val ); - if (success) { console.log('Cell updated successfully via WebSocket'); onSaveEnd?.(true); @@ -56,7 +52,7 @@ function EditCellPortal({ onError?.(errorMsg); onSaveEnd?.(false); // Возвращаем предыдущее значение - onChange(oldValue); + } } else { // Если updateCell нет в meta, используем onCellUpdate из пропсов (fallback) @@ -71,8 +67,7 @@ function EditCellPortal({ console.error('Error updating cell:', error); onError?.(error.message); onSaveEnd?.(false); - // Возвращаем предыдущее значение - onChange(oldValue); + } finally { setIsSaving(false); } @@ -118,11 +113,12 @@ function EditCellPortal({ ); } -export const getTableColumns = ({ - Cell, - EditCell, +export const getTableColumns = ({ + Cell, + EditCell, columnsConfig, - onCellUpdateError + onCellUpdateError, + onCellNumberClick }) => { const columnColors = { ...columnsConfig.colors }; const columns = [...columnsConfig.columns]; @@ -142,18 +138,12 @@ export const getTableColumns = ({ isUpdating={table.options.meta?.updatingCells?.[`${row.id}_${column.id}`]} /> ); - + col.Edit = ({ cell, table, row, column }) => ( { - // Локальное обновление кэша - if (row._valuesCache) { - row._valuesCache[column.id] = value; - } - }} onSaveStart={() => { if (table.options.meta?.setUpdatingCell) { table.options.meta.setUpdatingCell(row.id, column.id, true); @@ -186,8 +176,11 @@ export const getTableColumns = ({ size: 70, enableEditing: false, enablePinning: true, - Cell: ({ cell }) => { - return {cell.getValue()}; + Cell: ({ cell, row }) => { + return {console.log('TEST', row.id); onCellNumberClick(row.id)}} + >{cell.getValue()}; }, }; diff --git a/web/src/components/RealtimeTable/utils/cellUtils.js b/web/src/components/RealtimeTable/utils/cellUtils.js new file mode 100644 index 0000000..64b2e3d --- /dev/null +++ b/web/src/components/RealtimeTable/utils/cellUtils.js @@ -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; +}