387 lines
9.5 KiB
JavaScript
387 lines
9.5 KiB
JavaScript
import React, {
|
|
createContext,
|
|
useContext,
|
|
useCallback,
|
|
useRef,
|
|
useState,
|
|
useEffect,
|
|
} from "react";
|
|
import { useWebSocket } from "../hooks/useWebSocket";
|
|
import { toast } from "react-toastify";
|
|
import { getRowId } from "../utils/rowUtils";
|
|
|
|
const RealtimeContext = createContext(null);
|
|
|
|
export const useRealtime = () => {
|
|
const context = useContext(RealtimeContext);
|
|
if (!context) {
|
|
throw new Error("useRealtime must be used within RealtimeProvider");
|
|
}
|
|
return context;
|
|
};
|
|
|
|
export const RealtimeProvider = ({
|
|
children,
|
|
formId,
|
|
sheetName,
|
|
direction,
|
|
year,
|
|
userId,
|
|
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 [lockedCells, setLockedCells] = useState([]);
|
|
const handleMessage = useCallback((data) => {
|
|
// Обработка ошибок
|
|
if (data.error) {
|
|
console.log(data.error.message);
|
|
toast.error("Server error:" + data.error.message || "");
|
|
onErrorRef.current?.(data.error);
|
|
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":
|
|
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;
|
|
|
|
default:
|
|
console.log("Unknown event:", data.event);
|
|
}
|
|
}, []);
|
|
|
|
const { isConnectedRef, isConnected, sendMessage, disconnect, lastError } = useWebSocket(
|
|
wsUrl,
|
|
handleMessage,
|
|
);
|
|
|
|
const onCellUpdateRef = useRef(null);
|
|
const onRowAddRef = useRef(null);
|
|
const onRowDeleteRef = useRef(null);
|
|
const onErrorRef = useRef(null);
|
|
const onAuthSuccessRef = useRef(null);
|
|
const onCellEditStartRef = useRef(null);
|
|
const onCellEditEndRef = useRef(null);
|
|
const authAttemptedRef = useRef(false);
|
|
const reconnectTimerRef = useRef(null);
|
|
|
|
// Функция для получения токена из 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...");
|
|
setTimeout(() => {
|
|
sendLogin();
|
|
}, 100);
|
|
}
|
|
}, [isConnected, sendLogin]);
|
|
|
|
useEffect(() => {
|
|
if (!isConnected) {
|
|
authAttemptedRef.current = false;
|
|
}
|
|
}, [isConnected]);
|
|
|
|
const addCommonField = (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;
|
|
};
|
|
|
|
// Функция для начала редактирования ячейки
|
|
const startEditing = useCallback(
|
|
async (row, column) => {
|
|
const columnId = column.id;
|
|
const rowId = row.id;
|
|
|
|
if (!isConnectedRef) {
|
|
onErrorRef.current?.("WebSocket не подключен");
|
|
return false;
|
|
}
|
|
|
|
const lineId = row.id || null;
|
|
const colId = columnId.slice(5); // убираем префикс "data."
|
|
|
|
const message = {
|
|
event: "cell_edit_start",
|
|
data: {
|
|
line_id: lineId,
|
|
column: colId,
|
|
},
|
|
};
|
|
|
|
const sent = sendMessage(message);
|
|
if (!sent) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
[isConnected, sendMessage],
|
|
);
|
|
|
|
// Функция для завершения редактирования ячейки
|
|
const endEditing = useCallback(
|
|
async (row, column) => {
|
|
const columnId = column.id;
|
|
const rowId = row.id;
|
|
|
|
if (!isConnectedRef) {
|
|
onErrorRef.current?.("WebSocket не подключен");
|
|
return false;
|
|
}
|
|
|
|
const lineId = row.id || null;
|
|
const colId = columnId.slice(5);
|
|
|
|
const message = {
|
|
event: "cell_edit_end",
|
|
data: {
|
|
line_id: lineId,
|
|
column: colId,
|
|
},
|
|
};
|
|
|
|
const sent = sendMessage(message);
|
|
if (!sent) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
[isConnected, sendMessage],
|
|
);
|
|
|
|
const updateCell = useCallback(
|
|
async (row, column, value) => {
|
|
const columnId = column.id;
|
|
const rowId = row.id;
|
|
if (!isConnectedRef) {
|
|
onErrorRef.current?.("WebSocket не подключен");
|
|
return false;
|
|
}
|
|
|
|
const lineId = getRowId(row);
|
|
const colId = columnId.slice(5);
|
|
const message = {
|
|
event: "cell_updated",
|
|
data: {
|
|
line_id: lineId,
|
|
line_id_code: row.id,
|
|
column: colId,
|
|
value: value,
|
|
},
|
|
};
|
|
const sent = sendMessage(message);
|
|
|
|
if (!sent) {
|
|
return false;
|
|
}
|
|
|
|
return true;
|
|
},
|
|
[isConnected, sendMessage],
|
|
);
|
|
|
|
const addRow = useCallback(
|
|
async (rowData) => {
|
|
if (!isConnectedRef) {
|
|
onErrorRef.current?.("Нет подключения или авторизации");
|
|
return false;
|
|
}
|
|
|
|
const message = {
|
|
event: "row_added",
|
|
data: rowData,
|
|
};
|
|
addCommonField(message);
|
|
|
|
return sendMessage(message);
|
|
},
|
|
[isConnected, sendMessage, sheetName, formId, direction],
|
|
);
|
|
|
|
const deleteRow = useCallback(
|
|
async (rowId) => {
|
|
if (!isConnectedRef) {
|
|
onErrorRef.current?.("Нет подключения или авторизации");
|
|
return false;
|
|
}
|
|
|
|
const message = {
|
|
event: "row_deleted",
|
|
data: { row_id: rowId },
|
|
};
|
|
addCommonField(message);
|
|
|
|
return sendMessage(message);
|
|
},
|
|
[isConnected, sendMessage, sheetName, formId, direction],
|
|
);
|
|
|
|
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 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 (isConnected) {
|
|
authAttemptedRef.current = false;
|
|
sendLogin();
|
|
}
|
|
}, [isConnected, sendLogin]);
|
|
|
|
return (
|
|
<RealtimeContext.Provider
|
|
value={{
|
|
isConnected,
|
|
error: lastError,
|
|
startEditing,
|
|
endEditing,
|
|
updateCell,
|
|
addRow,
|
|
deleteRow,
|
|
subscribeToRowAdds,
|
|
subscribeToRowDeletes,
|
|
subscribeToErrors,
|
|
subscribeToAuthSuccess,
|
|
subscribeToCellEditStart,
|
|
subscribeToCellEditEnd,
|
|
subscribeToCellUpdates,
|
|
retryLogin,
|
|
lockedCells,
|
|
setLockedCells,
|
|
releaseAllLocks: () => releaseAllLocks(userId),
|
|
}}
|
|
>
|
|
{children}
|
|
</RealtimeContext.Provider>
|
|
);
|
|
};
|