- RealtimeContext: стор карты формул (formulaStore), загрузка при открытии листа и
переподключении (формы и отчёты проектов), обновление на каждом cell_updated,
защита от устаревших ответов при смене листа; useCellFormula; updateCell(..., { formula })
- Cell: маркер fx только при актуальной формуле, подсказка с единицей при displayFactor != 1
- EditCell: начальный текст «=выражение», выражение уходит в data.formula, без localStorage;
ввод «100+50» без «=» не отправляется (тост «Формула начинается с =»)
- удалены resolveEditorState и clearLegacyFormulaKeys; T6.1/T6.2 в cellFormulaUtils
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
708 lines
21 KiB
JavaScript
708 lines
21 KiB
JavaScript
import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, useSyncExternalStore } from 'react';
|
||
import { toast } from 'react-toastify';
|
||
import { FormsSheetApi } from '../../../api/form_sheet';
|
||
import { ProjectsApi } from '../../../api/projects';
|
||
import { useWebSocket } from '../hooks/useWebSocket';
|
||
import { clearLegacyFormulaStorage, formulaKey } from '../utils/cellFormulaUtils';
|
||
import { createFormulaStore } from '../utils/formulaStore';
|
||
import { buildCellUpdatedMessage } from '../utils/messageUtils';
|
||
import { getRowId } from '../utils/rowUtils';
|
||
import { getWsErrorMessage } from '../utils/wsErrorUtils';
|
||
|
||
const RealtimeActionsContext = createContext(null);
|
||
const RealtimeConnectionContext = createContext(null);
|
||
const RealtimeVspOptionsContext = createContext(null);
|
||
const LockedCellsStoreContext = createContext(null);
|
||
const FormulaStoreContext = createContext(null);
|
||
|
||
// AURORA-1896: формулы теперь хранятся на сервере — прежние ключи formula_* в localStorage
|
||
// удаляем один раз при загрузке модуля (идемпотентно, ошибки хранилища не критичны).
|
||
try {
|
||
clearLegacyFormulaStorage(window.localStorage);
|
||
} catch {
|
||
// localStorage недоступен (приватный режим и т.п.) — чистить нечего
|
||
}
|
||
|
||
export const getColumnRequestKey = (column) => {
|
||
const columnKey = column?.columnDef?.id ?? column?.id;
|
||
if (columnKey === null || columnKey === undefined) return '';
|
||
|
||
const normalizedColumnKey = String(columnKey);
|
||
return normalizedColumnKey.startsWith('data.') ? normalizedColumnKey.slice(5) : normalizedColumnKey;
|
||
};
|
||
|
||
const useRequiredContext = (context, hookName) => {
|
||
const value = useContext(context);
|
||
if (value === null) {
|
||
throw new Error(`${hookName} must be used within RealtimeProvider`);
|
||
}
|
||
return value;
|
||
};
|
||
|
||
const createLockedCellsStore = () => {
|
||
let cells = new Set();
|
||
const listenersByCell = new Map();
|
||
|
||
const subscribeCell = (cellKey, listener) => {
|
||
const listeners = listenersByCell.get(cellKey) || new Set();
|
||
listeners.add(listener);
|
||
listenersByCell.set(cellKey, listeners);
|
||
|
||
return () => {
|
||
listeners.delete(listener);
|
||
if (listeners.size === 0) listenersByCell.delete(cellKey);
|
||
};
|
||
};
|
||
|
||
const setCells = (nextValue) => {
|
||
const currentCells = [...cells];
|
||
const resolvedValue = typeof nextValue === 'function' ? nextValue(currentCells) : nextValue;
|
||
const nextCells = new Set(resolvedValue || []);
|
||
const changedCells = new Set([
|
||
...[...cells].filter((cellKey) => !nextCells.has(cellKey)),
|
||
...[...nextCells].filter((cellKey) => !cells.has(cellKey)),
|
||
]);
|
||
|
||
if (changedCells.size === 0) return;
|
||
cells = nextCells;
|
||
changedCells.forEach((cellKey) => {
|
||
listenersByCell.get(cellKey)?.forEach((listener) => listener());
|
||
});
|
||
};
|
||
|
||
return {
|
||
subscribeCell,
|
||
setCells,
|
||
hasCell: (cellKey) => cells.has(cellKey),
|
||
getCells: () => [...cells],
|
||
};
|
||
};
|
||
|
||
export const useRealtimeActions = () => useRequiredContext(RealtimeActionsContext, 'useRealtimeActions');
|
||
|
||
export const useRealtimeConnection = () => useRequiredContext(RealtimeConnectionContext, 'useRealtimeConnection');
|
||
|
||
export const useVspOptions = () => useRequiredContext(RealtimeVspOptionsContext, 'useVspOptions');
|
||
|
||
export const useCellLock = (cellKey) => {
|
||
const store = useRequiredContext(LockedCellsStoreContext, 'useCellLock');
|
||
const subscribe = useCallback((listener) => store.subscribeCell(cellKey, listener), [cellKey, store]);
|
||
const getSnapshot = useCallback(() => store.hasCell(cellKey), [cellKey, store]);
|
||
|
||
return useSyncExternalStore(subscribe, getSnapshot, () => false);
|
||
};
|
||
|
||
// AURORA-1891: имперактивная проверка lockedCellsStore.hasCell(key) из
|
||
// обработчика клика/клавиатуры на ячейке (muiTableBodyCellProps в
|
||
// RealtimeTable.jsx) — там нужен сам стор (не подписка на конкретную ячейку,
|
||
// как useCellLock выше), чтобы в момент события синхронно узнать, занята ли
|
||
// именно ЭТА ячейка, без лишнего ре-рендера на каждое изменение чужих блокировок.
|
||
export const useLockedCellsStore = () => useRequiredContext(LockedCellsStoreContext, 'useLockedCellsStore');
|
||
|
||
export const useFormulaStore = () => useRequiredContext(FormulaStoreContext, 'useFormulaStore');
|
||
|
||
// AURORA-1896: формула ячейки (line_id + ключ колонки без `data.`) или undefined.
|
||
// Подписка по ключу — при событии перерисовывается только затронутая ячейка.
|
||
export const useCellFormula = (lineId, columnKey) => {
|
||
const store = useFormulaStore();
|
||
const key = formulaKey(lineId, columnKey);
|
||
const subscribe = useCallback((listener) => store.subscribe(key, listener), [key, store]);
|
||
const getSnapshot = useCallback(() => store.get(key), [key, store]);
|
||
|
||
return useSyncExternalStore(subscribe, getSnapshot, () => undefined);
|
||
};
|
||
|
||
export const useRealtime = () => {
|
||
const actions = useRealtimeActions();
|
||
const connection = useRealtimeConnection();
|
||
const vspOptions = useVspOptions();
|
||
|
||
return { ...actions, ...connection, vspOptions };
|
||
};
|
||
|
||
export const RealtimeProvider = ({ children, formId, sheetName, direction, year, isProject = false }) => {
|
||
// Формируем URL (кастомные секреты на фронте пока недоступны, делаем через REACT_APP_ROOT_PATH)
|
||
const wsUrl = `${(
|
||
process.env.REACT_APP_API_URL || process.env.REACT_APP_API_URL || process.env.REACT_APP_ROOT_PATH || 'ws://localhost:8000'
|
||
).replace(/^http/, 'ws')}${
|
||
!isProject
|
||
? `/api/v1/ws/form/${formId}/sheet/${sheetName}${direction ? `?direction=${direction}` : ''}`
|
||
: `/api/v1/ws/projects/${formId}/report/${year}/${sheetName}`
|
||
}`;
|
||
|
||
const [vspOptions, setVspOptions] = useState([]);
|
||
const lockedCellsStoreRef = useRef(null);
|
||
if (lockedCellsStoreRef.current === null) {
|
||
lockedCellsStoreRef.current = createLockedCellsStore();
|
||
}
|
||
const lockedCellsStore = lockedCellsStoreRef.current;
|
||
const formulaStoreRef = useRef(null);
|
||
if (formulaStoreRef.current === null) {
|
||
formulaStoreRef.current = createFormulaStore();
|
||
}
|
||
const formulaStore = formulaStoreRef.current;
|
||
|
||
const handleMessage = useCallback((data) => {
|
||
// Обработка ошибок
|
||
if (data.error) {
|
||
console.log(data.error);
|
||
toast.error(getWsErrorMessage(data.error));
|
||
onErrorRef.current?.(data.error);
|
||
// AURORA-1980: окну переименования нужно полное сообщение (event + data),
|
||
// а не только текст ошибки.
|
||
if (data.event === 'program_updated' || data.event === 'project_updated') {
|
||
onNodeRenameResultRef.current?.(data);
|
||
}
|
||
return;
|
||
}
|
||
|
||
switch (data.event) {
|
||
case 'cell_edit_start':
|
||
console.log('Cell edit started:', data);
|
||
if (onCellEditStartRef.current) {
|
||
onCellEditStartRef.current(data);
|
||
}
|
||
break;
|
||
|
||
case 'cell_edit_end':
|
||
console.log('Cell edit ended:', data);
|
||
if (onCellEditEndRef.current) {
|
||
onCellEditEndRef.current(data);
|
||
}
|
||
break;
|
||
|
||
case 'cell_updated':
|
||
// AURORA-1896: ответы с ошибкой отсекаются выше (data.error), сюда доходят только
|
||
// успешные события — и свои, и чужие. Фоновое событие без line_id/column карту не меняет.
|
||
formulaStoreRef.current.applyEvent(data.data);
|
||
if (data.result && onCellUpdateRef.current) {
|
||
const updatedCells = Array.isArray(data.result) ? data.result : [data.result];
|
||
onCellUpdateRef.current(updatedCells);
|
||
}
|
||
break;
|
||
|
||
case 'row_added':
|
||
if (data.result && onRowAddRef.current) {
|
||
onRowAddRef.current(data);
|
||
}
|
||
break;
|
||
|
||
case 'row_deleted':
|
||
if (data.result && onRowDeleteRef.current) {
|
||
onRowDeleteRef.current(data);
|
||
}
|
||
break;
|
||
|
||
case 'program_added':
|
||
console.log('Program added:', data);
|
||
if (data.result && onProgramAddRef.current) {
|
||
onProgramAddRef.current(data);
|
||
}
|
||
break;
|
||
|
||
case 'project_added':
|
||
console.log('Project added:', data);
|
||
if (data.result && onProjectAddRef.current) {
|
||
onProjectAddRef.current(data);
|
||
}
|
||
break;
|
||
|
||
// AURORA-1978: удаление программы/проекта формы 4.
|
||
case 'program_deleted':
|
||
case 'project_deleted':
|
||
if (data.result && onNodeDeleteRef.current) {
|
||
onNodeDeleteRef.current(data);
|
||
}
|
||
break;
|
||
|
||
// AURORA-1980: переименование программы/проекта формы 4.
|
||
case 'program_updated':
|
||
case 'project_updated':
|
||
if (data.result) {
|
||
onNodeUpdateRef.current?.(data);
|
||
onNodeRenameResultRef.current?.(data);
|
||
}
|
||
break;
|
||
|
||
default:
|
||
console.log('Unknown event:', data.event);
|
||
}
|
||
}, []);
|
||
|
||
const { isConnectedRef, isConnected, sendMessage } = useWebSocket(wsUrl, handleMessage);
|
||
|
||
// AURORA-1896: смена листа/формы/года — карта прежнего листа не должна протекать.
|
||
// Ответы загрузок, начатых до сброса, стор отбрасывает по токену (reset/beginLoad).
|
||
// biome-ignore lint/correctness/useExhaustiveDependencies: идентификаторы листа — триггеры сброса карты
|
||
useEffect(() => {
|
||
formulaStore.reset();
|
||
return () => formulaStore.reset();
|
||
}, [formulaStore, formId, sheetName, year, isProject]);
|
||
|
||
// Карта формул грузится при открытии листа и при каждом (пере)подключении WS:
|
||
// за время обрыва другие пользователи могли изменить формулы.
|
||
// biome-ignore lint/correctness/useExhaustiveDependencies: isConnected — триггер перезагрузки при переподключении
|
||
useEffect(() => {
|
||
if (formId === undefined || formId === null || !sheetName) return;
|
||
const token = formulaStore.beginLoad();
|
||
const request = isProject ? ProjectsApi.getReportFormulas(formId, year, sheetName) : FormsSheetApi.getFormulas(formId, sheetName);
|
||
Promise.resolve(request)
|
||
.then((list) => {
|
||
formulaStore.finishLoad(token, Array.isArray(list) ? list : []);
|
||
})
|
||
.catch((error) => {
|
||
console.warn('Не удалось загрузить формулы листа:', error);
|
||
formulaStore.failLoad(token);
|
||
});
|
||
}, [formulaStore, formId, sheetName, year, isProject, isConnected]);
|
||
|
||
const onCellUpdateRef = useRef(null);
|
||
const onRowAddRef = useRef(null);
|
||
const onRowDeleteRef = useRef(null);
|
||
const onProgramAddRef = useRef(null);
|
||
const onProjectAddRef = useRef(null);
|
||
const onNodeDeleteRef = useRef(null);
|
||
const onNodeUpdateRef = useRef(null);
|
||
const onNodeRenameResultRef = useRef(null);
|
||
const onErrorRef = useRef(null);
|
||
const onAuthSuccessRef = useRef(null);
|
||
const onCellEditStartRef = useRef(null);
|
||
const onCellEditEndRef = useRef(null);
|
||
const authAttemptedRef = useRef(false);
|
||
|
||
// Функция для получения токена из localStorage
|
||
const getAccessToken = useCallback(() => {
|
||
try {
|
||
const token = localStorage.getItem('access_token');
|
||
|
||
if (!token) {
|
||
console.warn('No access token found in localStorage');
|
||
return null;
|
||
}
|
||
|
||
return token;
|
||
} catch (error) {
|
||
console.error('Error reading token from localStorage:', error);
|
||
return null;
|
||
}
|
||
}, []);
|
||
|
||
const sendLogin = useCallback(() => {
|
||
const token = getAccessToken();
|
||
if (!token) {
|
||
console.error('Cannot login: no token available');
|
||
onErrorRef.current?.('Токен авторизации не найден');
|
||
return false;
|
||
}
|
||
|
||
const loginMessage = {
|
||
event: 'user_login',
|
||
data: { token },
|
||
};
|
||
|
||
return sendMessage(loginMessage);
|
||
}, [getAccessToken, sendMessage]);
|
||
|
||
useEffect(() => {
|
||
if (isConnected && !authAttemptedRef.current) {
|
||
authAttemptedRef.current = true;
|
||
console.log('WebSocket connected, sending login...');
|
||
const timeoutId = setTimeout(() => {
|
||
sendLogin();
|
||
}, 100);
|
||
return () => clearTimeout(timeoutId);
|
||
}
|
||
}, [isConnected, sendLogin]);
|
||
|
||
useEffect(() => {
|
||
if (!isConnected) {
|
||
authAttemptedRef.current = false;
|
||
}
|
||
}, [isConnected]);
|
||
|
||
const addCommonField = useCallback(
|
||
(message) => {
|
||
if (isProject) {
|
||
message.report_type = sheetName;
|
||
message.project_id = formId;
|
||
message.year = year;
|
||
} else {
|
||
message.sheet = sheetName;
|
||
message.form_id = formId;
|
||
message.direction = direction;
|
||
}
|
||
return message;
|
||
},
|
||
[direction, formId, isProject, sheetName, year],
|
||
);
|
||
|
||
// Функция для начала редактирования ячейки
|
||
const startEditing = useCallback(
|
||
(row, column) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('WebSocket не подключен');
|
||
return false;
|
||
}
|
||
|
||
const lineId = row.id || null;
|
||
const columnKey = getColumnRequestKey(column);
|
||
|
||
const message = {
|
||
event: 'cell_edit_start',
|
||
data: {
|
||
line_id: lineId,
|
||
column: columnKey,
|
||
},
|
||
};
|
||
|
||
const sent = sendMessage(message);
|
||
if (!sent) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
},
|
||
[isConnectedRef, sendMessage],
|
||
);
|
||
|
||
// Функция для завершения редактирования ячейки
|
||
const endEditing = useCallback(
|
||
(row, column) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('WebSocket не подключен');
|
||
return false;
|
||
}
|
||
|
||
const lineId = row.id || null;
|
||
const columnKey = getColumnRequestKey(column);
|
||
|
||
const message = {
|
||
event: 'cell_edit_end',
|
||
data: {
|
||
line_id: lineId,
|
||
column: columnKey,
|
||
},
|
||
};
|
||
|
||
const sent = sendMessage(message);
|
||
if (!sent) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
},
|
||
[isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const updateCell = useCallback(
|
||
(row, column, value, { formula } = {}) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('WebSocket не подключен');
|
||
return false;
|
||
}
|
||
|
||
const lineId = getRowId(row);
|
||
const columnKey = getColumnRequestKey(column);
|
||
const message = buildCellUpdatedMessage({ lineId, lineIdCode: row.id, column: columnKey, value, formula });
|
||
const sent = sendMessage(message);
|
||
|
||
if (!sent) {
|
||
return false;
|
||
}
|
||
|
||
return true;
|
||
},
|
||
[isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const addRow = useCallback(
|
||
(rowData) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'row_added',
|
||
data: rowData,
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const addProject = useCallback(
|
||
(rowData) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'project_added',
|
||
data: rowData,
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const addProgram = useCallback(
|
||
(rowData) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'program_added',
|
||
data: rowData,
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const deleteRow = useCallback(
|
||
(rowId) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'row_deleted',
|
||
data: { row_id: rowId },
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
// AURORA-1978: программа/проект формы 4 удаляются своими событиями —
|
||
// у их строк нет line_id, row_deleted для них не подходит.
|
||
const deleteProgram = useCallback(
|
||
(programId) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'program_deleted',
|
||
data: { program_id: programId },
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const deleteProject = useCallback(
|
||
(projectId) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: 'project_deleted',
|
||
data: { project_id: projectId },
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
// AURORA-1980: переименование программы/проекта формы 4.
|
||
const renameNode = useCallback(
|
||
(kind, id, name) => {
|
||
if (!isConnectedRef.current) {
|
||
onErrorRef.current?.('Нет подключения или авторизации');
|
||
return false;
|
||
}
|
||
|
||
const message = {
|
||
event: `${kind}_updated`,
|
||
data: { [`${kind}_id`]: id, name },
|
||
};
|
||
addCommonField(message);
|
||
|
||
return sendMessage(message);
|
||
},
|
||
[addCommonField, isConnectedRef, sendMessage],
|
||
);
|
||
|
||
const subscribeToCellUpdates = useCallback((callback) => {
|
||
onCellUpdateRef.current = callback;
|
||
return () => {
|
||
onCellUpdateRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToRowAdds = useCallback((callback) => {
|
||
onRowAddRef.current = callback;
|
||
return () => {
|
||
onRowAddRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToRowDeletes = useCallback((callback) => {
|
||
onRowDeleteRef.current = callback;
|
||
return () => {
|
||
onRowDeleteRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToProgramAdds = useCallback((callback) => {
|
||
onProgramAddRef.current = callback;
|
||
return () => {
|
||
onProgramAddRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToProjectAdds = useCallback((callback) => {
|
||
onProjectAddRef.current = callback;
|
||
return () => {
|
||
onProjectAddRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToNodeDeletes = useCallback((callback) => {
|
||
onNodeDeleteRef.current = callback;
|
||
return () => {
|
||
onNodeDeleteRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToNodeUpdates = useCallback((callback) => {
|
||
onNodeUpdateRef.current = callback;
|
||
return () => {
|
||
onNodeUpdateRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToNodeRenameResult = useCallback((callback) => {
|
||
onNodeRenameResultRef.current = callback;
|
||
return () => {
|
||
onNodeRenameResultRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToErrors = useCallback((callback) => {
|
||
onErrorRef.current = callback;
|
||
return () => {
|
||
onErrorRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToAuthSuccess = useCallback((callback) => {
|
||
onAuthSuccessRef.current = callback;
|
||
return () => {
|
||
onAuthSuccessRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToCellEditStart = useCallback((callback) => {
|
||
onCellEditStartRef.current = callback;
|
||
return () => {
|
||
onCellEditStartRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const subscribeToCellEditEnd = useCallback((callback) => {
|
||
onCellEditEndRef.current = callback;
|
||
return () => {
|
||
onCellEditEndRef.current = null;
|
||
};
|
||
}, []);
|
||
|
||
const retryLogin = useCallback(() => {
|
||
if (isConnectedRef.current) {
|
||
authAttemptedRef.current = false;
|
||
sendLogin();
|
||
}
|
||
}, [isConnectedRef, sendLogin]);
|
||
|
||
const actions = useMemo(
|
||
() => ({
|
||
startEditing,
|
||
endEditing,
|
||
updateCell,
|
||
addRow,
|
||
addProgram,
|
||
addProject,
|
||
deleteRow,
|
||
deleteProgram,
|
||
deleteProject,
|
||
renameNode,
|
||
subscribeToRowAdds,
|
||
subscribeToRowDeletes,
|
||
subscribeToProgramAdds,
|
||
subscribeToProjectAdds,
|
||
subscribeToNodeDeletes,
|
||
subscribeToNodeUpdates,
|
||
subscribeToNodeRenameResult,
|
||
subscribeToErrors,
|
||
subscribeToAuthSuccess,
|
||
subscribeToCellEditStart,
|
||
subscribeToCellEditEnd,
|
||
subscribeToCellUpdates,
|
||
retryLogin,
|
||
setLockedCells: lockedCellsStore.setCells,
|
||
setVspOptions,
|
||
}),
|
||
[
|
||
addProgram,
|
||
addProject,
|
||
addRow,
|
||
deleteProgram,
|
||
deleteProject,
|
||
deleteRow,
|
||
endEditing,
|
||
renameNode,
|
||
lockedCellsStore,
|
||
retryLogin,
|
||
startEditing,
|
||
subscribeToAuthSuccess,
|
||
subscribeToCellEditEnd,
|
||
subscribeToCellEditStart,
|
||
subscribeToCellUpdates,
|
||
subscribeToErrors,
|
||
subscribeToNodeDeletes,
|
||
subscribeToNodeRenameResult,
|
||
subscribeToNodeUpdates,
|
||
subscribeToProgramAdds,
|
||
subscribeToProjectAdds,
|
||
subscribeToRowAdds,
|
||
subscribeToRowDeletes,
|
||
updateCell,
|
||
],
|
||
);
|
||
|
||
const connection = useMemo(() => ({ isConnected }), [isConnected]);
|
||
|
||
return (
|
||
<RealtimeActionsContext.Provider value={actions}>
|
||
<RealtimeConnectionContext.Provider value={connection}>
|
||
<RealtimeVspOptionsContext.Provider value={vspOptions}>
|
||
<LockedCellsStoreContext.Provider value={lockedCellsStore}>
|
||
<FormulaStoreContext.Provider value={formulaStore}>{children}</FormulaStoreContext.Provider>
|
||
</LockedCellsStoreContext.Provider>
|
||
</RealtimeVspOptionsContext.Provider>
|
||
</RealtimeConnectionContext.Provider>
|
||
</RealtimeActionsContext.Provider>
|
||
);
|
||
};
|