Merge branch 'develop' into iframe

This commit is contained in:
Rohit
2025-01-04 15:51:51 +05:30
committed by GitHub
56 changed files with 3858 additions and 657 deletions

View File

@@ -19,6 +19,7 @@ import styled from 'styled-components';
import axios from 'axios';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { apiUrl } from '../../apiConfig';
import { useTranslation } from 'react-i18next';
const Container = styled(Box)`
display: flex;
@@ -29,24 +30,21 @@ const Container = styled(Box)`
`;
const ApiKeyManager = () => {
const { t } = useTranslation();
const [apiKey, setApiKey] = useState<string | null>(null);
const [apiKeyName, setApiKeyName] = useState<string>('Maxun API Key');
const [apiKeyName, setApiKeyName] = useState<string>(t('apikey.default_name'));
const [loading, setLoading] = useState<boolean>(true);
const [showKey, setShowKey] = useState<boolean>(false);
const [copySuccess, setCopySuccess] = useState<boolean>(false);
const { notify } = useGlobalInfoStore();
useEffect(() => {
const fetchApiKey = async () => {
try {
const { data } = await axios.get(`${apiUrl}/auth/api-key`);
setApiKey(data.api_key);
} catch (error: any) {
notify('error', `Failed to fetch API Key - ${error.message}`);
notify('error', t('apikey.notifications.fetch_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -62,9 +60,9 @@ const ApiKeyManager = () => {
const { data } = await axios.post(`${apiUrl}/auth/generate-api-key`);
setApiKey(data.api_key);
notify('success', `Generated API Key successfully`);
notify('success', t('apikey.notifications.generate_success'));
} catch (error: any) {
notify('error', `Failed to generate API Key - ${error.message}`);
notify('error', t('apikey.notifications.generate_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -75,9 +73,9 @@ const ApiKeyManager = () => {
try {
await axios.delete(`${apiUrl}/auth/delete-api-key`);
setApiKey(null);
notify('success', 'API Key deleted successfully');
notify('success', t('apikey.notifications.delete_success'));
} catch (error: any) {
notify('error', `Failed to delete API Key - ${error.message}`);
notify('error', t('apikey.notifications.delete_error', { error: error.message }));
} finally {
setLoading(false);
}
@@ -88,7 +86,7 @@ const ApiKeyManager = () => {
navigator.clipboard.writeText(apiKey);
setCopySuccess(true);
setTimeout(() => setCopySuccess(false), 2000);
notify('info', 'Copied API Key successfully');
notify('info', t('apikey.notifications.copy_success'));
}
};
@@ -111,34 +109,38 @@ const ApiKeyManager = () => {
return (
<Container sx={{ alignSelf: 'flex-start' }}>
<Typography variant="h6" gutterBottom component="div" style={{ marginBottom: '20px' }}>
Manage Your API Key
{t('apikey.title')}
</Typography>
{apiKey ? (
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden' }}>
<Table>
<TableHead>
<TableRow>
<TableCell>API Key Name</TableCell>
<TableCell>API Key</TableCell>
<TableCell>Actions</TableCell>
<TableCell>{t('apikey.table.name')}</TableCell>
<TableCell>{t('apikey.table.key')}</TableCell>
<TableCell>{t('apikey.table.actions')}</TableCell>
</TableRow>
</TableHead>
<TableBody>
<TableRow>
<TableCell>{apiKeyName}</TableCell>
<TableCell>{showKey ? `${apiKey?.substring(0, 10)}...` : '***************'}</TableCell>
<TableCell>
<Tooltip title="Copy">
<Box sx={{ fontFamily: 'monospace', width: '10ch' }}>
{showKey ? `${apiKey?.substring(0, 10)}...` : '**********'}
</Box>
</TableCell>
<TableCell>
<Tooltip title={t('apikey.actions.copy')}>
<IconButton onClick={copyToClipboard}>
<ContentCopy />
</IconButton>
</Tooltip>
<Tooltip title={showKey ? 'Hide' : 'Show'}>
<Tooltip title={showKey ? t('apikey.actions.hide') : t('apikey.actions.show')}>
<IconButton onClick={() => setShowKey(!showKey)}>
<Visibility />
</IconButton>
</Tooltip>
<Tooltip title="Delete">
<Tooltip title={t('apikey.actions.delete')}>
<IconButton onClick={deleteApiKey} color="error">
<Delete />
</IconButton>
@@ -150,9 +152,9 @@ const ApiKeyManager = () => {
</TableContainer>
) : (
<>
<Typography>You haven't generated an API key yet.</Typography>
<Typography>{t('apikey.no_key_message')}</Typography>
<Button onClick={generateApiKey} variant="contained" color="primary" sx={{ marginTop: '15px' }}>
Generate API Key
{t('apikey.generate_button')}
</Button>
</>
)}

View File

@@ -7,6 +7,7 @@ import { GenericModal } from '../atoms/GenericModal';
import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, TextStep } from '../../context/browserSteps';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next';
interface ElementInfo {
@@ -53,6 +54,7 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
};
export const BrowserWindow = () => {
const { t } = useTranslation();
const [canvasRef, setCanvasReference] = useState<React.RefObject<HTMLCanvasElement> | undefined>(undefined);
const [screenShot, setScreenShot] = useState<string>("");
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
@@ -67,7 +69,7 @@ export const BrowserWindow = () => {
const { socket } = useSocketStore();
const { notify } = useGlobalInfoStore();
const { getText, getList, paginationMode, paginationType, limitMode } = useActionContext();
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
const { addTextStep, addListStep } = useBrowserSteps();
const onMouseMove = (e: MouseEvent) => {
@@ -169,7 +171,7 @@ export const BrowserWindow = () => {
// For non-list steps
setHighlighterData(data);
}
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]);
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType, captureStage]);
useEffect(() => {
@@ -183,6 +185,13 @@ export const BrowserWindow = () => {
};
}, [socket, onMouseMove]);
useEffect(() => {
if (captureStage === 'initial' && listSelector) {
socket?.emit('setGetList', { getList: true });
socket?.emit('listSelector', { selector: listSelector });
}
}, [captureStage, listSelector, socket]);
const handleClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (highlighterData && canvasRef?.current) {
const canvasRect = canvasRef.current.getBoundingClientRect();
@@ -227,7 +236,7 @@ export const BrowserWindow = () => {
// Only allow selection in pagination mode if type is not empty, 'scrollDown', or 'scrollUp'
if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') {
setPaginationSelector(highlighterData.selector);
notify(`info`, `Pagination element selected successfully.`);
notify(`info`, t('browser_window.attribute_modal.notifications.pagination_select_success'));
addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector });
}
return;
@@ -235,7 +244,7 @@ export const BrowserWindow = () => {
if (getList === true && !listSelector) {
setListSelector(highlighterData.selector);
notify(`info`, `List selected succesfully. Select the text data for extraction.`)
notify(`info`, t('browser_window.attribute_modal.notifications.list_select_success'));
setCurrentListId(Date.now());
setFields({});
} else if (getList === true && listSelector && currentListId) {

View File

@@ -5,6 +5,9 @@ import Box from '@mui/material/Box';
import { Paper, Button } from "@mui/material";
import { AutoAwesome, FormatListBulleted, VpnKey, Usb, CloudQueue, Code } from "@mui/icons-material";
import { apiUrl } from "../../apiConfig";
import { useTranslation } from 'react-i18next';
import i18n from '../../i18n';
interface MainMenuProps {
value: string;
@@ -12,6 +15,7 @@ interface MainMenuProps {
}
export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenuProps) => {
const {t} = useTranslation();
const handleChange = (event: React.SyntheticEvent, newValue: string) => {
handleChangeContent(newValue);
@@ -47,7 +51,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="recordings"
label="Robots"
label={t('mainmenu.recordings')}
icon={<AutoAwesome />}
iconPosition="start"
/>
@@ -58,7 +62,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="runs"
label="Runs"
label={t('mainmenu.runs')}
icon={<FormatListBulleted />}
iconPosition="start"
/>
@@ -69,7 +73,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="proxy"
label="Proxy"
label={t('mainmenu.proxy')}
icon={<Usb />}
iconPosition="start"
/>
@@ -80,7 +84,7 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
fontSize: 'medium',
}}
value="apikey"
label="API Key"
label={t('mainmenu.apikey')}
icon={<VpnKey />}
iconPosition="start"
/>
@@ -88,10 +92,10 @@ export const MainMenu = ({ value = 'recordings', handleChangeContent }: MainMenu
<hr />
<Box sx={{ display: 'flex', flexDirection: 'column', gap: '1rem', textAlign: 'left' }}>
<Button href={`${apiUrl}/api-docs/`} target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<Code />}>
Website To API
{t('mainmenu.apidocs')}
</Button>
<Button href="https://forms.gle/hXjgqDvkEhPcaBW76" target="_blank" rel="noopener noreferrer" sx={buttonStyles} startIcon={<CloudQueue />}>
Join Maxun Cloud
{t('mainmenu.feedback')}
</Button>
</Box>
</Box>

View File

@@ -3,6 +3,7 @@ import { styled } from '@mui/system';
import { Alert, AlertTitle, TextField, Button, Switch, FormControlLabel, Box, Typography, Tabs, Tab, Table, TableContainer, TableHead, TableRow, TableBody, TableCell, Paper } from '@mui/material';
import { sendProxyConfig, getProxyConfig, testProxyConfig, deleteProxyConfig } from '../../api/proxy';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { useTranslation } from 'react-i18next';
const FormContainer = styled(Box)({
display: 'flex',
@@ -16,6 +17,7 @@ const FormControl = styled(Box)({
});
const ProxyForm: React.FC = () => {
const { t } = useTranslation();
const [proxyConfigForm, setProxyConfigForm] = useState({
server_url: '',
username: '',
@@ -79,13 +81,13 @@ const ProxyForm: React.FC = () => {
try {
const response = await sendProxyConfig(proxyConfigForm);
if (response) {
notify('success', 'Proxy configuration submitted successfully');
notify('success', t('proxy.notifications.config_success'));
} else {
notify('error', `Failed to submit proxy configuration. Try again. ${response}`);
console.log(`Failed to submit proxy configuration. Try again. ${response}`)
notify('error', t('proxy.notifications.config_error'));
console.log(`${t('proxy.notifications.config_error')} ${response}`)
}
} catch (error: any) {
notify('error', `${error} : Failed to submit proxy configuration`);
notify('error', `${error} : ${t('proxy.notifications.config_error')}`);
}
};
@@ -96,9 +98,9 @@ const ProxyForm: React.FC = () => {
const testProxy = async () => {
await testProxyConfig().then((response) => {
if (response.success) {
notify('success', 'Proxy configuration is working');
notify('success', t('proxy.notifications.test_success'));
} else {
notify('error', 'Failed to test proxy configuration. Try again.');
notify('error', t('proxy.notifications.test_error'));
}
});
};
@@ -109,7 +111,7 @@ const ProxyForm: React.FC = () => {
if (response.proxy_url) {
setIsProxyConfigured(true);
setProxy(response);
notify('success', 'Proxy configuration fetched successfully');
notify('success', t('proxy.notifications.fetch_success'));
}
} catch (error: any) {
notify('error', error);
@@ -119,11 +121,11 @@ const ProxyForm: React.FC = () => {
const removeProxy = async () => {
await deleteProxyConfig().then((response) => {
if (response) {
notify('success', 'Proxy configuration removed successfully');
notify('success', t('proxy.notifications.remove_success'));
setIsProxyConfigured(false);
setProxy({ proxy_url: '', auth: false });
} else {
notify('error', 'Failed to remove proxy configuration. Try again.');
notify('error', t('proxy.notifications.remove_error'));
}
});
}
@@ -136,11 +138,11 @@ const ProxyForm: React.FC = () => {
<>
<FormContainer>
<Typography variant="h6" gutterBottom component="div" style={{ marginTop: '20px' }}>
Proxy Configuration
{t('proxy.title')}
</Typography>
<Tabs value={tabIndex} onChange={handleTabChange}>
<Tab label="Standard Proxy" />
<Tab label="Automatic Proxy Rotation" />
<Tab label={t('proxy.tab_standard')} />
<Tab label={t('proxy.tab_rotation')} />
</Tabs>
{tabIndex === 0 && (
isProxyConfigured ? (
@@ -149,8 +151,8 @@ const ProxyForm: React.FC = () => {
<Table>
<TableHead>
<TableRow>
<TableCell><strong>Proxy URL</strong></TableCell>
<TableCell><strong>Requires Authentication</strong></TableCell>
<TableCell><strong>{t('proxy.table.proxy_url')}</strong></TableCell>
<TableCell><strong>{t('proxy.table.requires_auth')}</strong></TableCell>
</TableRow>
</TableHead>
<TableBody>
@@ -162,39 +164,37 @@ const ProxyForm: React.FC = () => {
</Table>
</TableContainer>
<Button variant="outlined" color="primary" onClick={testProxy}>
Test Proxy
{t('proxy.test_proxy')}
</Button>
<Button variant="outlined" color="error" onClick={removeProxy} sx={{ marginLeft: '10px' }}>
Remove Proxy
{t('proxy.remove_proxy')}
</Button>
</Box>
) : (
<Box component="form" onSubmit={handleSubmit} sx={{ maxWidth: 400, width: '100%' }}>
<FormControl>
<TextField
label="Proxy Server URL"
label={t('proxy.server_url')}
name="server_url"
value={proxyConfigForm.server_url}
onChange={handleChange}
fullWidth
required
error={!!errors.server_url}
helperText={errors.server_url || `Proxy to be used for all robots. HTTP and SOCKS proxies are supported.
Example http://myproxy.com:3128 or socks5://myproxy.com:3128.
Short form myproxy.com:3128 is considered an HTTP proxy.`}
helperText={errors.server_url || t('proxy.server_url_helper')}
/>
</FormControl>
<FormControl>
<FormControlLabel
control={<Switch checked={requiresAuth} onChange={handleAuthToggle} />}
label="Requires Authentication?"
label={t('proxy.requires_auth')}
/>
</FormControl>
{requiresAuth && (
<>
<FormControl>
<TextField
label="Username"
label={t('proxy.username')}
name="username"
value={proxyConfigForm.username}
onChange={handleChange}
@@ -206,7 +206,7 @@ const ProxyForm: React.FC = () => {
</FormControl>
<FormControl>
<TextField
label="Password"
label={t('proxy.password')}
name="password"
value={proxyConfigForm.password}
onChange={handleChange}
@@ -226,7 +226,7 @@ const ProxyForm: React.FC = () => {
fullWidth
disabled={!proxyConfigForm.server_url || (requiresAuth && (!proxyConfigForm.username || !proxyConfigForm.password))}
>
Add Proxy
{t('proxy.add_proxy')}
</Button>
</Box>
))}
@@ -234,33 +234,33 @@ const ProxyForm: React.FC = () => {
<Box sx={{ maxWidth: 400, width: '100%', textAlign: 'center', marginTop: '20px' }}>
<>
<Typography variant="body1" gutterBottom component="div">
Coming Soon - In Open Source (Basic Rotation) & Cloud (Advanced Rotation). If you don't want to manage the infrastructure, join our cloud waitlist to get early access.
{t('proxy.coming_soon')}
</Typography>
<Button variant="contained" color="primary" sx={{ marginTop: '20px' }}>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">Join Maxun Cloud Waitlist</a>
<a style={{ color: 'white', textDecoration: 'none' }} href="https://forms.gle/hXjgqDvkEhPcaBW76">{t('proxy.join_waitlist')}</a>
</Button>
</>
</Box>
)}
</FormContainer>
<Alert severity="info" sx={{ marginTop: '80px', marginLeft: '50px', height: '230px', width: '450px', border: '1px solid #ff00c3' }}>
<AlertTitle>If your proxy requires a username and password, always provide them separately from the proxy URL. </AlertTitle>
<AlertTitle>{t('proxy.alert.title')}</AlertTitle>
<br />
<b>The right way</b>
<b>{t('proxy.alert.right_way')}</b>
<br />
Proxy URL: http://proxy.com:1337
{t('proxy.alert.proxy_url')} http://proxy.com:1337
<br />
Username: myusername
{t('proxy.alert.username')} myusername
<br />
Password: mypassword
{t('proxy.alert.password')} mypassword
<br />
<br />
<b>The wrong way</b>
<b>{t('proxy.alert.wrong_way')}</b>
<br />
Proxy URL: http://myusername:mypassword@proxy.com:1337
{t('proxy.alert.proxy_url')} http://myusername:mypassword@proxy.com:1337
</Alert>
</>
);
};
export default ProxyForm;
export default ProxyForm;

View File

@@ -22,6 +22,7 @@ import { emptyWorkflow } from "../../shared/constants";
import { getActiveWorkflow } from "../../api/workflow";
import DeleteIcon from '@mui/icons-material/Delete';
import ActionDescriptionBox from '../molecules/ActionDescriptionBox';
import { useTranslation } from 'react-i18next';
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
@@ -55,11 +56,14 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const [showCaptureText, setShowCaptureText] = useState(true);
const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({});
const [browserStepIdList, setBrowserStepIdList] = useState<number[]>([]);
const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false);
const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false);
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState } = useGlobalInfoStore();
const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog } = useGlobalInfoStore();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage } = useActionContext();
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps();
const { id, socket } = useSocketStore();
const { t } = useTranslation();
const workflowHandler = useCallback((data: WorkflowFile) => {
setWorkflow(data);
@@ -128,6 +132,16 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const handlePairDelete = () => { }
const handleStartGetText = () => {
setIsCaptureTextConfirmed(false);
startGetText();
}
const handleStartGetList = () => {
setIsCaptureListConfirmed(false);
startGetList();
}
const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => {
if (listId !== undefined && fieldKey !== undefined) {
// Prevent editing if the field is confirmed
@@ -139,7 +153,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setTextLabels(prevLabels => ({ ...prevLabels, [id]: label }));
}
if (!label.trim()) {
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
} else {
setErrors(prevErrors => ({ ...prevErrors, [id]: '' }));
}
@@ -151,7 +165,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
updateBrowserTextStepLabel(id, label);
setConfirmedTextSteps(prev => ({ ...prev, [id]: true }));
} else {
setErrors(prevErrors => ({ ...prevErrors, [id]: 'Label cannot be empty' }));
setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') }));
}
};
@@ -167,6 +181,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
});
};
const handleTextStepDelete = (id: number) => {
deleteBrowserStep(id);
setTextLabels(prevLabels => {
const { [id]: _, ...rest } = prevLabels;
return rest;
});
setConfirmedTextSteps(prev => {
const { [id]: _, ...rest } = prev;
return rest;
});
setErrors(prevErrors => {
const { [id]: _, ...rest } = prevErrors;
return rest;
});
};
const handleListTextFieldConfirm = (listId: number, fieldKey: string) => {
setConfirmedListTextFields(prev => ({
...prev,
@@ -193,6 +223,22 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
});
};
const handleListTextFieldDelete = (listId: number, fieldKey: string) => {
removeListTextField(listId, fieldKey);
setConfirmedListTextFields(prev => {
const updatedListFields = { ...(prev[listId] || {}) };
delete updatedListFields[fieldKey];
return {
...prev,
[listId]: updatedListFields
};
});
setErrors(prev => {
const { [fieldKey]: _, ...rest } = prev;
return rest;
});
};
const getTextSettingsObject = useCallback(() => {
const settings: Record<string, { selector: string; tag?: string;[key: string]: any }> = {};
browserSteps.forEach(step => {
@@ -213,18 +259,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
const stopCaptureAndEmitGetTextSettings = useCallback(() => {
const hasUnconfirmedTextSteps = browserSteps.some(step => step.type === 'text' && !confirmedTextSteps[step.id]);
if (hasUnconfirmedTextSteps) {
notify('error', 'Please confirm all text fields');
notify('error', t('right_panel.errors.confirm_text_fields'));
return;
}
stopGetText();
const settings = getTextSettingsObject();
console.log("SETTINGS", settings);
const hasTextSteps = browserSteps.some(step => step.type === 'text');
if (hasTextSteps) {
socket?.emit('action', { action: 'scrapeSchema', settings });
}
setIsCaptureTextConfirmed(true);
resetInterpretationLog();
onFinishCapture();
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]);
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog]);
const getListSettingsObject = useCallback(() => {
let settings: {
@@ -278,7 +325,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
if (settings) {
socket?.emit('action', { action: 'scrapeList', settings });
} else {
notify('error', 'Unable to create list settings. Make sure you have defined a field for the list.');
notify('error', t('right_panel.errors.unable_create_settings'));
}
handleStopGetList();
onFinishCapture();
@@ -296,13 +343,13 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
case 'pagination':
if (!paginationType) {
notify('error', 'Please select a pagination type.');
notify('error', t('right_panel.errors.select_pagination'));
return;
}
const settings = getListSettingsObject();
const paginationSelector = settings.pagination?.selector;
if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) {
notify('error', 'Please select the pagination element first.');
notify('error', t('right_panel.errors.select_pagination_element'));
return;
}
stopPaginationMode();
@@ -314,16 +361,17 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
case 'limit':
if (!limitType || (limitType === 'custom' && !customLimit)) {
notify('error', 'Please select a limit or enter a custom limit.');
notify('error', t('right_panel.errors.select_limit'));
return;
}
const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType);
if (isNaN(limit) || limit <= 0) {
notify('error', 'Please enter a valid limit.');
notify('error', t('right_panel.errors.invalid_limit'));
return;
}
stopLimitMode();
setShowLimitOptions(false);
setIsCaptureListConfirmed(true);
stopCaptureAndEmitGetListSettings();
setCaptureStage('complete');
break;
@@ -334,6 +382,23 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}
}, [captureStage, paginationType, limitType, customLimit, startPaginationMode, stopPaginationMode, startLimitMode, stopLimitMode, notify, stopCaptureAndEmitGetListSettings, getListSettingsObject]);
const handleBackCaptureList = useCallback(() => {
switch (captureStage) {
case 'limit':
stopLimitMode();
setShowLimitOptions(false);
startPaginationMode();
setShowPaginationOptions(true);
setCaptureStage('pagination');
break;
case 'pagination':
stopPaginationMode();
setShowPaginationOptions(false);
setCaptureStage('initial');
break;
}
}, [captureStage, stopLimitMode, startPaginationMode, stopPaginationMode]);
const handlePaginationSettingSelect = (option: PaginationType) => {
updatePaginationType(option);
};
@@ -348,7 +413,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setTextLabels({});
setErrors({});
setConfirmedTextSteps({});
notify('error', 'Capture Text Discarded');
setIsCaptureTextConfirmed(false);
notify('error', t('right_panel.errors.capture_text_discarded'));
}, [browserSteps, stopGetText, deleteBrowserStep]);
const discardGetList = useCallback(() => {
@@ -363,7 +429,8 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
setShowLimitOptions(false);
setCaptureStage('initial');
setConfirmedListTextFields({});
notify('error', 'Capture List Discarded');
setIsCaptureListConfirmed(false);
notify('error', t('right_panel.errors.capture_list_discarded'));
}, [browserSteps, stopGetList, deleteBrowserStep, resetListState]);
@@ -402,37 +469,48 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</SimpleBox> */}
<ActionDescriptionBox />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>Capture List</Button>}
{!getText && !getScreenshot && !getList && showCaptureList && <Button variant="contained" onClick={startGetList}>{t('right_panel.buttons.capture_list')}</Button>}
{getList && (
<>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
{(captureStage === 'pagination' || captureStage === 'limit') && (
<Button
variant="outlined"
onClick={handleBackCaptureList}
>
{t('right_panel.buttons.back')}
</Button>
)}
<Button
variant="outlined"
onClick={handleConfirmListCapture}
disabled={captureStage === 'initial' ? isConfirmCaptureDisabled : hasUnconfirmedListTextFields}
>
{captureStage === 'initial' ? 'Confirm Capture' :
captureStage === 'pagination' ? 'Confirm Pagination' :
captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'}
{captureStage === 'initial' ? t('right_panel.buttons.confirm_capture') :
captureStage === 'pagination' ? t('right_panel.buttons.confirm_pagination') :
captureStage === 'limit' ? t('right_panel.buttons.confirm_limit') :
t('right_panel.buttons.finish_capture')}
</Button>
<Button variant="outlined" color="error" onClick={discardGetList}>
{t('right_panel.buttons.discard')}
</Button>
<Button variant="outlined" color="error" onClick={discardGetList}>Discard</Button>
</Box>
</>
)}
{showPaginationOptions && (
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '13px' }}>
<Typography>How can we find the next list item on the page?</Typography>
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>Click on next to navigate to the next page</Button>
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>Click on load more to load more items</Button>
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>Scroll down to load more items</Button>
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>Scroll up to load more items</Button>
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>No more items to load</Button>
<Typography>{t('right_panel.pagination.title')}</Typography>
<Button variant={paginationType === 'clickNext' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickNext')}>{t('right_panel.pagination.click_next')}</Button>
<Button variant={paginationType === 'clickLoadMore' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('clickLoadMore')}>{t('right_panel.pagination.click_load_more')}</Button>
<Button variant={paginationType === 'scrollDown' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollDown')}>{t('right_panel.pagination.scroll_down')}</Button>
<Button variant={paginationType === 'scrollUp' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('scrollUp')}>{t('right_panel.pagination.scroll_up')}</Button>
<Button variant={paginationType === 'none' ? "contained" : "outlined"} onClick={() => handlePaginationSettingSelect('none')}>{t('right_panel.pagination.none')}</Button>
</Box>
)}
{showLimitOptions && (
<FormControl>
<FormLabel>
<h4>What is the maximum number of rows you want to extract?</h4>
<h4>{t('right_panel.limit.title')}</h4>
</FormLabel>
<RadioGroup
value={limitType}
@@ -446,13 +524,28 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<FormControlLabel value="10" control={<Radio />} label="10" />
<FormControlLabel value="100" control={<Radio />} label="100" />
<div style={{ display: 'flex', alignItems: 'center' }}>
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
<FormControlLabel value="custom" control={<Radio />} label={t('right_panel.limit.custom')} />
{limitType === 'custom' && (
<TextField
type="number"
value={customLimit}
onChange={(e) => updateCustomLimit(e.target.value)}
placeholder="Enter number"
onChange={(e: React.ChangeEvent<HTMLInputElement>) => {
const value = parseInt(e.target.value);
// Only update if the value is greater than or equal to 1 or if the field is empty
if (e.target.value === '' || value >= 1) {
updateCustomLimit(e.target.value);
}
}}
inputProps={{
min: 1,
onKeyPress: (e: React.KeyboardEvent<HTMLInputElement>) => {
const value = (e.target as HTMLInputElement).value + e.key;
if (parseInt(value) < 1) {
e.preventDefault();
}
}
}}
placeholder={t('right_panel.limit.enter_number')}
sx={{
marginLeft: '10px',
'& input': {
@@ -467,21 +560,21 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
</RadioGroup>
</FormControl>
)}
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={startGetText}>Capture Text</Button>}
{!getText && !getScreenshot && !getList && showCaptureText && <Button variant="contained" onClick={handleStartGetText}>{t('right_panel.buttons.capture_text')}</Button>}
{getText &&
<>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >Confirm</Button>
<Button variant="outlined" color="error" onClick={discardGetText} >Discard</Button>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >{t('right_panel.buttons.confirm')}</Button>
<Button variant="outlined" color="error" onClick={discardGetText} >{t('right_panel.buttons.discard')}</Button>
</Box>
</>
}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>Capture Screenshot</Button>}
{!getText && !getScreenshot && !getList && showCaptureScreenshot && <Button variant="contained" onClick={startGetScreenshot}>{t('right_panel.buttons.capture_screenshot')}</Button>}
{getScreenshot && (
<Box display="flex" flexDirection="column" gap={2}>
<Button variant="contained" onClick={() => captureScreenshot(true)}>Capture Fullpage</Button>
<Button variant="contained" onClick={() => captureScreenshot(false)}>Capture Visible Part</Button>
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>Discard</Button>
<Button variant="contained" onClick={() => captureScreenshot(true)}>{t('right_panel.screenshot.capture_fullpage')}</Button>
<Button variant="contained" onClick={() => captureScreenshot(false)}>{t('right_panel.screenshot.capture_visible')}</Button>
<Button variant="outlined" color="error" onClick={stopGetScreenshot}>{t('right_panel.buttons.discard')}</Button>
</Box>
)}
</Box>
@@ -492,7 +585,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
step.type === 'text' && (
<>
<TextField
label="Label"
label={t('right_panel.fields.label')}
value={textLabels[step.id] || step.label || ''}
onChange={(e) => handleTextLabelChange(step.id, e.target.value)}
fullWidth
@@ -510,7 +603,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}}
/>
<TextField
label="Data"
label={t('right_panel.fields.data')}
value={step.data}
fullWidth
margin="normal"
@@ -523,10 +616,20 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
)
}}
/>
{!confirmedTextSteps[step.id] && (
{!confirmedTextSteps[step.id] ? (
<Box display="flex" justifyContent="space-between" gap={2}>
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>Confirm</Button>
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>Discard</Button>
<Button variant="contained" onClick={() => handleTextStepConfirm(step.id)} disabled={!textLabels[step.id]?.trim()}>{t('right_panel.buttons.confirm')}</Button>
<Button variant="contained" color="error" onClick={() => handleTextStepDiscard(step.id)}>{t('right_panel.buttons.discard')}</Button>
</Box>
) : !isCaptureTextConfirmed && (
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
color="error"
onClick={() => handleTextStepDelete(step.id)}
>
{t('right_panel.buttons.delete')}
</Button>
</Box>
)}
</>
@@ -535,17 +638,19 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
<Box display="flex" alignItems="center">
<DocumentScannerIcon sx={{ mr: 1 }} />
<Typography>
{`Take ${step.fullPage ? 'Fullpage' : 'Visible Part'} Screenshot`}
{step.fullPage ?
t('right_panel.screenshot.display_fullpage') :
t('right_panel.screenshot.display_visible')}
</Typography>
</Box>
)}
{step.type === 'list' && (
<>
<Typography>List Selected Successfully</Typography>
<Typography>{t('right_panel.messages.list_selected')}</Typography>
{Object.entries(step.fields).map(([key, field]) => (
<Box key={key}>
<TextField
label="Field Label"
label={t('right_panel.fields.field_label')}
value={field.label || ''}
onChange={(e) => handleTextLabelChange(field.id, e.target.value, step.id, key)}
fullWidth
@@ -560,7 +665,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
}}
/>
<TextField
label="Field Data"
label={t('right_panel.fields.field_data')}
value={field.data || ''}
fullWidth
margin="normal"
@@ -573,21 +678,31 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
)
}}
/>
{!confirmedListTextFields[step.id]?.[key] && (
{!confirmedListTextFields[step.id]?.[key] ? (
<Box display="flex" justifyContent="space-between" gap={2}>
<Button
variant="contained"
onClick={() => handleListTextFieldConfirm(step.id, key)}
disabled={!field.label?.trim()}
>
Confirm
{t('right_panel.buttons.confirm')}
</Button>
<Button
variant="contained"
color="error"
onClick={() => handleListTextFieldDiscard(step.id, key)}
>
Discard
{t('right_panel.buttons.discard')}
</Button>
</Box>
) : !isCaptureListConfirmed && (
<Box display="flex" justifyContent="flex-end" gap={2}>
<Button
variant="contained"
color="error"
onClick={() => handleListTextFieldDelete(step.id, key)}
>
{t('right_panel.buttons.delete')}
</Button>
</Box>
)}