Merge branch 'develop' into ui-fix
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
import { useReducer, createContext, useEffect } from 'react';
|
||||
import { useReducer, createContext, useEffect, useCallback } from 'react';
|
||||
import axios from 'axios';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import { apiUrl } from "../apiConfig";
|
||||
@@ -14,12 +14,16 @@ interface ActionType {
|
||||
|
||||
type InitialStateType = {
|
||||
user: any;
|
||||
lastActivityTime?: number;
|
||||
};
|
||||
|
||||
const initialState = {
|
||||
user: null,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
|
||||
const AUTO_LOGOUT_TIME = 4 * 60 * 60 * 1000; // 4 hours in milliseconds
|
||||
|
||||
const AuthContext = createContext<{
|
||||
state: InitialStateType;
|
||||
dispatch: React.Dispatch<ActionType>;
|
||||
@@ -34,11 +38,13 @@ const reducer = (state: InitialStateType, action: ActionType) => {
|
||||
return {
|
||||
...state,
|
||||
user: action.payload,
|
||||
lastActivityTime: Date.now(),
|
||||
};
|
||||
case 'LOGOUT':
|
||||
return {
|
||||
...state,
|
||||
user: null,
|
||||
lastActivityTime: undefined,
|
||||
};
|
||||
default:
|
||||
return state;
|
||||
@@ -50,6 +56,39 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
const navigate = useNavigate();
|
||||
axios.defaults.withCredentials = true;
|
||||
|
||||
const handleLogout = useCallback(async () => {
|
||||
try {
|
||||
await axios.get(`${apiUrl}/auth/logout`);
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
} catch (err) {
|
||||
console.error('Logout error:', err);
|
||||
}
|
||||
}, [navigate]);
|
||||
|
||||
const checkAutoLogout = useCallback(() => {
|
||||
if (state.user && state.lastActivityTime) {
|
||||
const currentTime = Date.now();
|
||||
const timeSinceLastActivity = currentTime - state.lastActivityTime;
|
||||
|
||||
if (timeSinceLastActivity >= AUTO_LOGOUT_TIME) {
|
||||
handleLogout();
|
||||
}
|
||||
}
|
||||
}, [state.user, state.lastActivityTime, handleLogout]);
|
||||
|
||||
// Update last activity time on user interactions
|
||||
const updateActivityTime = useCallback(() => {
|
||||
if (state.user) {
|
||||
dispatch({
|
||||
type: 'LOGIN',
|
||||
payload: state.user // Reuse existing user data
|
||||
});
|
||||
}
|
||||
}, [state.user]);
|
||||
|
||||
// Initialize user from localStorage
|
||||
useEffect(() => {
|
||||
const storedUser = window.localStorage.getItem('user');
|
||||
if (storedUser) {
|
||||
@@ -57,21 +96,54 @@ const AuthProvider = ({ children }: AuthProviderProps) => {
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Set up activity listeners
|
||||
useEffect(() => {
|
||||
if (state.user) {
|
||||
// List of events to track for user activity
|
||||
const events = ['mousedown', 'keydown', 'scroll', 'touchstart'];
|
||||
|
||||
// Throttled event handler
|
||||
let timeoutId: NodeJS.Timeout;
|
||||
const handleActivity = () => {
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
timeoutId = setTimeout(updateActivityTime, 1000);
|
||||
};
|
||||
|
||||
// Add event listeners
|
||||
events.forEach(event => {
|
||||
window.addEventListener(event, handleActivity);
|
||||
});
|
||||
|
||||
// Set up periodic check for auto logout
|
||||
const checkInterval = setInterval(checkAutoLogout, 60000); // Check every minute
|
||||
|
||||
// Cleanup
|
||||
return () => {
|
||||
events.forEach(event => {
|
||||
window.removeEventListener(event, handleActivity);
|
||||
});
|
||||
clearInterval(checkInterval);
|
||||
if (timeoutId) {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
};
|
||||
}
|
||||
}, [state.user, updateActivityTime, checkAutoLogout]);
|
||||
|
||||
axios.interceptors.response.use(
|
||||
function (response) {
|
||||
return response;
|
||||
},
|
||||
function (error) {
|
||||
const res = error.response;
|
||||
if (res.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||
return new Promise((resolve, reject) => {
|
||||
axios
|
||||
.get(`${apiUrl}/auth/logout`)
|
||||
if (res?.status === 401 && res.config && !res.config.__isRetryRequest) {
|
||||
return new Promise((_, reject) => {
|
||||
handleLogout()
|
||||
.then(() => {
|
||||
console.log('/401 error > logout');
|
||||
dispatch({ type: 'LOGOUT' });
|
||||
window.localStorage.removeItem('user');
|
||||
navigate('/login');
|
||||
reject(error);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error('AXIOS INTERCEPTORS ERROR:', err);
|
||||
|
||||
@@ -14,8 +14,8 @@ interface ActionContextProps {
|
||||
paginationType: PaginationType;
|
||||
limitType: LimitType;
|
||||
customLimit: string;
|
||||
captureStage: CaptureStage; // New captureStage property
|
||||
setCaptureStage: (stage: CaptureStage) => void; // Setter for captureStage
|
||||
captureStage: CaptureStage;
|
||||
setCaptureStage: (stage: CaptureStage) => void;
|
||||
startPaginationMode: () => void;
|
||||
startGetText: () => void;
|
||||
stopGetText: () => void;
|
||||
@@ -53,6 +53,7 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => {
|
||||
const startPaginationMode = () => {
|
||||
setPaginationMode(true);
|
||||
setCaptureStage('pagination');
|
||||
socket?.emit('setGetList', { getList: false });
|
||||
};
|
||||
|
||||
const stopPaginationMode = () => setPaginationMode(false);
|
||||
@@ -75,7 +76,6 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => {
|
||||
|
||||
const stopGetList = () => {
|
||||
setGetList(false);
|
||||
socket?.emit('setGetList', { getList: false });
|
||||
setPaginationType('');
|
||||
setLimitType('');
|
||||
setCustomLimit('');
|
||||
|
||||
@@ -32,6 +32,7 @@ export interface SelectorObject {
|
||||
selector: string;
|
||||
tag?: string;
|
||||
attribute?: string;
|
||||
shadow?: boolean;
|
||||
[key: string]: any;
|
||||
}
|
||||
|
||||
@@ -62,26 +63,35 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
||||
const addListStep = (listSelector: string, newFields: { [key: string]: TextStep }, listId: number, pagination?: { type: string; selector: string }, limit?: number) => {
|
||||
setBrowserSteps(prevSteps => {
|
||||
const existingListStepIndex = prevSteps.findIndex(step => step.type === 'list' && step.id === listId);
|
||||
|
||||
if (existingListStepIndex !== -1) {
|
||||
const updatedSteps = [...prevSteps];
|
||||
const existingListStep = updatedSteps[existingListStepIndex] as ListStep;
|
||||
|
||||
const filteredNewFields = Object.entries(newFields).reduce((acc, [key, value]) => {
|
||||
|
||||
// Preserve existing labels for fields
|
||||
const mergedFields = Object.entries(newFields).reduce((acc, [key, field]) => {
|
||||
if (!discardedFields.has(`${listId}-${key}`)) {
|
||||
acc[key] = value;
|
||||
// If field exists, preserve its label
|
||||
if (existingListStep.fields[key]) {
|
||||
acc[key] = {
|
||||
...field,
|
||||
label: existingListStep.fields[key].label
|
||||
};
|
||||
} else {
|
||||
acc[key] = field;
|
||||
}
|
||||
}
|
||||
return acc;
|
||||
}, {} as { [key: string]: TextStep });
|
||||
|
||||
|
||||
updatedSteps[existingListStepIndex] = {
|
||||
...existingListStep,
|
||||
fields: { ...existingListStep.fields, ...filteredNewFields },
|
||||
pagination: pagination,
|
||||
limit: limit,
|
||||
fields: mergedFields,
|
||||
pagination: pagination || existingListStep.pagination,
|
||||
limit: limit
|
||||
};
|
||||
return updatedSteps;
|
||||
} else {
|
||||
// Create a new ListStep
|
||||
return [
|
||||
...prevSteps,
|
||||
{ id: listId, type: 'list', listSelector, fields: newFields, pagination, limit }
|
||||
|
||||
@@ -32,6 +32,8 @@ interface GlobalInfo {
|
||||
hasScreenshotAction: boolean;
|
||||
hasScrapeSchemaAction: boolean;
|
||||
}) => void;
|
||||
shouldResetInterpretationLog: boolean;
|
||||
resetInterpretationLog: () => void;
|
||||
};
|
||||
|
||||
class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
@@ -53,6 +55,7 @@ class GlobalInfoStore implements Partial<GlobalInfo> {
|
||||
hasScreenshotAction: false,
|
||||
hasScrapeSchemaAction: false,
|
||||
};
|
||||
shouldResetInterpretationLog = false;
|
||||
};
|
||||
|
||||
const globalInfoStore = new GlobalInfoStore();
|
||||
@@ -71,6 +74,7 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
const [recordingName, setRecordingName] = useState<string>(globalInfoStore.recordingName);
|
||||
const [recordingUrl, setRecordingUrl] = useState<string>(globalInfoStore.recordingUrl);
|
||||
const [currentWorkflowActionsState, setCurrentWorkflowActionsState] = useState(globalInfoStore.currentWorkflowActionsState);
|
||||
const [shouldResetInterpretationLog, setShouldResetInterpretationLog] = useState<boolean>(globalInfoStore.shouldResetInterpretationLog);
|
||||
|
||||
const notify = (severity: 'error' | 'warning' | 'info' | 'success', message: string) => {
|
||||
setNotification({ severity, message, isOpen: true });
|
||||
@@ -87,6 +91,14 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
}
|
||||
}
|
||||
|
||||
const resetInterpretationLog = () => {
|
||||
setShouldResetInterpretationLog(true);
|
||||
// Reset the flag after a short delay to allow components to respond
|
||||
setTimeout(() => {
|
||||
setShouldResetInterpretationLog(false);
|
||||
}, 100);
|
||||
}
|
||||
|
||||
return (
|
||||
<globalInfoContext.Provider
|
||||
value={{
|
||||
@@ -111,6 +123,8 @@ export const GlobalInfoProvider = ({ children }: { children: JSX.Element }) => {
|
||||
setRecordingUrl,
|
||||
currentWorkflowActionsState,
|
||||
setCurrentWorkflowActionsState,
|
||||
shouldResetInterpretationLog,
|
||||
resetInterpretationLog,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user