Merge pull request #24 from amhsirak/develop

feat: handle `scrapeList` while browser recording
This commit is contained in:
Karishma Shukla
2024-09-08 14:10:39 +05:30
committed by GitHub
15 changed files with 700 additions and 263 deletions

View File

@@ -421,7 +421,6 @@ async function clickNextPagination(selector, scrapedData, limit) {
return results;
};
window.scrollDown = async function (selector, limit) {
let previousHeight = 0;
let itemsLoaded = 0;

View File

@@ -7,6 +7,7 @@ import {
getElementInformation,
getRect,
getSelectors,
getChildSelectors,
getNonUniqueSelectors,
isRuleOvershadowing,
selectorAlreadyInWorkflow
@@ -53,6 +54,8 @@ export class WorkflowGenerator {
*/
private getList: boolean = false;
private listSelector: string = '';
/**
* The public constructor of the WorkflowGenerator.
* Takes socket for communication as a parameter and registers some important events on it.
@@ -103,6 +106,9 @@ export class WorkflowGenerator {
this.socket.on('setGetList', (data: { getList: boolean }) => {
this.getList = data.getList;
});
this.socket.on('listSelector', (data: { selector: string }) => {
this.listSelector = data.selector;
})
}
/**
@@ -476,6 +482,11 @@ export class WorkflowGenerator {
*/
private generateSelector = async (page: Page, coordinates: Coordinates, action: ActionType) => {
const elementInfo = await getElementInformation(page, coordinates);
const generalSelector = await getNonUniqueSelectors(page, coordinates)
const childSelectors = await getChildSelectors(page, generalSelector.generalSelector);
console.log('Non Unique Selectors [DEBUG]:', generalSelector);
console.log('Child Selectors [DEBUG]:', childSelectors);
const selectorBasedOnCustomAction = (this.getList === true)
? await getNonUniqueSelectors(page, coordinates)
@@ -507,7 +518,14 @@ export class WorkflowGenerator {
const displaySelector = await this.generateSelector(page, coordinates, ActionType.Click);
const elementInfo = await getElementInformation(page, coordinates);
if (rect) {
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
if (this.getList === true) {
if (this.listSelector !== '') {
const childSelectors = await getChildSelectors(page, this.listSelector || '');
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo, childSelectors })
}
} else {
this.socket.emit('highlighter', { rect, selector: displaySelector, elementInfo });
}
}
// reset getList after usage
this.getList = false;

View File

@@ -4,6 +4,11 @@ import { WhereWhatPair, WorkflowFile } from "maxun-core";
import logger from "../logger";
import { getBestSelectorForAction } from "./utils";
/*TODO:
1. Handle TS errors (here we definetly know better)
2. Add pending function descriptions + thought process (esp. selector generation)
*/
type Workflow = WorkflowFile["workflow"];
/**
@@ -97,21 +102,6 @@ export const getElementInformation = async (
},
{ x: coordinates.x, y: coordinates.y },
);
// if (elementInfo) {
// if (elementInfo.tagName === 'A') {
// if (elementInfo.innerText) {
// console.log(`Link text: ${elementInfo.innerText}, URL: ${elementInfo.url}`);
// } else {
// console.log(`URL: ${elementInfo.url}`);
// }
// } else if (elementInfo.tagName === 'IMG') {
// console.log(`Image URL: ${elementInfo.imageUrl}`);
// } else {
// console.log(`Element innerText: ${elementInfo.innerText}`);
// }
// }
return elementInfo;
} catch (error) {
const { message, stack } = error as Error;
@@ -591,8 +581,6 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => {
return output;
}
const genSelectors = (element: HTMLElement | null) => {
if (element == null) {
return null;
@@ -722,6 +710,10 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => {
};
interface SelectorResult {
generalSelector: string;
}
/**
* Returns the best non-unique css {@link Selectors} for the element on the page.
* @param page The page instance.
@@ -730,18 +722,16 @@ export const getSelectors = async (page: Page, coordinates: Coordinates) => {
* @returns {Promise<Selectors|null|undefined>}
*/
export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates) => {
export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates): Promise<SelectorResult> => {
try {
const selectors = await page.evaluate(({ x, y }: { x: number, y: number }) => {
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
// Avoid using IDs to maintain non-uniqueness
if (element.className) {
const classes = element.className.split(/\s+/).filter((cls: string) => Boolean(cls));
if (classes.length > 0) {
// Exclude utility classes and escape special characters
const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':'));
if (validClasses.length > 0) {
selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.');
@@ -754,11 +744,16 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
function getSelectorPath(element: HTMLElement | null): string {
const path: string[] = [];
while (element && element !== document.body) {
let depth = 0;
const maxDepth = 2;
while (element && element !== document.body && depth < maxDepth) {
const selector = getNonUniqueSelector(element);
path.unshift(selector);
element = element.parentElement;
depth++;
}
return path.join(' > ');
}
@@ -771,15 +766,67 @@ export const getNonUniqueSelectors = async (page: Page, coordinates: Coordinates
};
}, coordinates);
return selectors || {};
return selectors || { generalSelector: '' };
} catch (error) {
console.error('Error in getNonUniqueSelectors:', error);
return {};
return { generalSelector: '' };
}
};
export const getChildSelectors = async (page: Page, parentSelector: string): Promise<string[]> => {
try {
const childSelectors = await page.evaluate((parentSelector: string) => {
function getNonUniqueSelector(element: HTMLElement): string {
let selector = element.tagName.toLowerCase();
const className = typeof element.className === 'string' ? element.className : '';
if (className) {
const classes = className.split(/\s+/).filter((cls: string) => Boolean(cls));
if (classes.length > 0) {
const validClasses = classes.filter((cls: string) => !cls.startsWith('!') && !cls.includes(':'));
if (validClasses.length > 0) {
selector += '.' + validClasses.map(cls => CSS.escape(cls)).join('.');
}
}
}
return selector;
}
function getSelectorPath(element: HTMLElement | null): string {
if (!element || !element.parentElement) return '';
const parentSelector = getNonUniqueSelector(element.parentElement);
const elementSelector = getNonUniqueSelector(element);
return `${parentSelector} > ${elementSelector}`;
}
function getAllDescendantSelectors(element: HTMLElement, stopAtParent: HTMLElement | null): string[] {
let selectors: string[] = [];
const children = Array.from(element.children) as HTMLElement[];
for (const child of children) {
selectors.push(getSelectorPath(child));
selectors = selectors.concat(getAllDescendantSelectors(child, stopAtParent));
}
return selectors;
}
const parentElement = document.querySelector(parentSelector) as HTMLElement;
if (!parentElement) return [];
return getAllDescendantSelectors(parentElement, parentElement);
}, parentSelector);
return childSelectors || [];
} catch (error) {
console.error('Error in getChildSelectors:', error);
return [];
}
};
/**
* Returns the first pair from the given workflow that contains the given selector

View File

@@ -1,7 +1,7 @@
import React, { useRef } from 'react';
import styled from "styled-components";
import { Button } from "@mui/material";
import { ActionDescription } from "../organisms/RightSidePanel";
//import { ActionDescription } from "../organisms/RightSidePanel";
import * as Settings from "./action-settings";
import { useSocketStore } from "../../context/socket";
@@ -42,7 +42,7 @@ export const ActionSettings = ({ action }: ActionSettingsProps) => {
return (
<div>
<ActionDescription>Action settings:</ActionDescription>
{/* <ActionDescription>Action settings:</ActionDescription> */}
<ActionSettingsWrapper action={action}>
<form onSubmit={handleSubmit}>
<DisplaySettings />

View File

@@ -1,5 +1,5 @@
import type {
FC,
FC,
} from 'react';
import styled from 'styled-components';
@@ -8,9 +8,9 @@ import ArrowBackIcon from '@mui/icons-material/ArrowBack';
import ArrowForwardIcon from '@mui/icons-material/ArrowForward';
import { NavBarButton } from '../atoms/buttons/buttons';
import { UrlForm } from './UrlForm';
import { UrlForm } from './UrlForm';
import { useCallback, useEffect, useState } from "react";
import {useSocketStore} from "../../context/socket";
import { useSocketStore } from "../../context/socket";
import { getCurrentUrl } from "../../api/recording";
const StyledNavBar = styled.div<{ browserWidth: number }>`
@@ -21,8 +21,8 @@ const StyledNavBar = styled.div<{ browserWidth: number }>`
`;
interface NavBarProps {
browserWidth: number;
handleUrlChanged: (url: string) => void;
browserWidth: number;
handleUrlChanged: (url: string) => void;
};
const BrowserNavBar: FC<NavBarProps> = ({
@@ -30,16 +30,15 @@ const BrowserNavBar: FC<NavBarProps> = ({
handleUrlChanged,
}) => {
// context:
const { socket } = useSocketStore();
const [currentUrl, setCurrentUrl] = useState<string>('https://');
const handleRefresh = useCallback(() : void => {
const handleRefresh = useCallback((): void => {
socket?.emit('input:refresh');
}, [socket]);
const handleGoTo = useCallback((address: string) : void => {
const handleGoTo = useCallback((address: string): void => {
socket?.emit('input:url', address);
}, [socket]);
@@ -70,54 +69,54 @@ const BrowserNavBar: FC<NavBarProps> = ({
}
}, [socket, handleCurrentUrlChange])
const addAddress = (address: string) => {
if (socket) {
handleUrlChanged(address);
handleGoTo(address);
}
};
const addAddress = (address: string) => {
if (socket) {
handleUrlChanged(address);
handleGoTo(address);
}
};
return (
<StyledNavBar browserWidth={browserWidth}>
<NavBarButton
type="button"
onClick={() => {
socket?.emit('input:back');
}}
disabled={false}
>
<ArrowBackIcon/>
</NavBarButton>
return (
<StyledNavBar browserWidth={browserWidth}>
<NavBarButton
type="button"
onClick={() => {
socket?.emit('input:back');
}}
disabled={false}
>
<ArrowBackIcon />
</NavBarButton>
<NavBarButton
type="button"
onClick={()=>{
socket?.emit('input:forward');
}}
disabled={false}
>
<ArrowForwardIcon/>
</NavBarButton>
<NavBarButton
type="button"
onClick={() => {
socket?.emit('input:forward');
}}
disabled={false}
>
<ArrowForwardIcon />
</NavBarButton>
<NavBarButton
type="button"
onClick={() => {
if (socket) {
handleRefresh()
}
}}
disabled={false}
>
<ReplayIcon/>
</NavBarButton>
<NavBarButton
type="button"
onClick={() => {
if (socket) {
handleRefresh()
}
}}
disabled={false}
>
<ReplayIcon />
</NavBarButton>
<UrlForm
currentAddress={currentUrl}
handleRefresh={handleRefresh}
setCurrentAddress={addAddress}
/>
</StyledNavBar>
);
<UrlForm
currentAddress={currentUrl}
handleRefresh={handleRefresh}
setCurrentAddress={addAddress}
/>
</StyledNavBar>
);
}
export default BrowserNavBar;

View File

@@ -29,59 +29,60 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
actionType: string,
selector: string,
action: string,
open:boolean
}>({ pair: null, actionType: '', selector: '', action: '', open: false} );
open: boolean
}>({ pair: null, actionType: '', selector: '', action: '', open: false });
const { socket } = useSocketStore();
const { notify } = useGlobalInfoStore();
const finishedHandler = useCallback(() => {
setInfo({...info, isPaused: false});
setInfo({ ...info, isPaused: false });
enableStepping(false);
}, [info, enableStepping]);
const breakpointHitHandler = useCallback(() => {
setInfo({running: false, isPaused: true});
setInfo({ running: false, isPaused: true });
notify('warning', 'Please restart the interpretation, after updating the recording');
enableStepping(true);
}, [info, enableStepping]);
const decisionHandler = useCallback(
({pair, actionType, lastData}
: {pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string }}) => {
const {selector, action} = lastData;
setDecisionModal((prevState) => {
return {
pair,
actionType,
selector,
action,
open: true,
}
})
}, [decisionModal]);
({ pair, actionType, lastData }
: { pair: WhereWhatPair | null, actionType: string, lastData: { selector: string, action: string } }) => {
const { selector, action } = lastData;
setDecisionModal((prevState) => {
return {
pair,
actionType,
selector,
action,
open: true,
}
})
}, [decisionModal]);
const handleDecision = (decision: boolean) => {
const {pair, actionType} = decisionModal;
socket?.emit('decision', {pair, actionType, decision});
setDecisionModal({pair: null, actionType: '', selector: '', action: '', open: false});
const { pair, actionType } = decisionModal;
socket?.emit('decision', { pair, actionType, decision });
setDecisionModal({ pair: null, actionType: '', selector: '', action: '', open: false });
}
const handleDescription = () => {
switch (decisionModal.actionType){
switch (decisionModal.actionType) {
case 'customAction':
return (
<React.Fragment>
<Typography>
Do you want to use the previously recorded selector
as a where condition for matching the action?
</Typography>
<Box style={{marginTop: '4px'}}>
[previous action: <b>{decisionModal.action}</b>]
<pre>{decisionModal.selector}</pre>
</Box>
<Typography>
Do you want to use the previously recorded selector
as a where condition for matching the action?
</Typography>
<Box style={{ marginTop: '4px' }}>
[previous action: <b>{decisionModal.action}</b>]
<pre>{decisionModal.selector}</pre>
</Box>
</React.Fragment>);
default: return null;}
default: return null;
}
}
useEffect(() => {
@@ -100,12 +101,12 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
const handlePlay = async () => {
if (info.isPaused) {
socket?.emit("resume");
setInfo({running: true, isPaused: false});
setInfo({ running: true, isPaused: false });
enableStepping(false);
} else {
setInfo({...info, running: true});
setInfo({ ...info, running: true });
const finished = await interpretCurrentRecording();
setInfo({...info, running: false});
setInfo({ ...info, running: false });
if (finished) {
notify('info', 'Interpretation finished');
} else {
@@ -131,45 +132,45 @@ export const InterpretationButtons = ({ enableStepping }: InterpretationButtonsP
return (
<Stack direction="row" spacing={3}
sx={{ marginTop: '10px', marginBottom: '5px', justifyContent: 'space-evenly',}} >
<IconButton disabled={!info.running} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
aria-label="pause" size="small" title="Pause" onClick={handlePause}>
<PauseCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
sx={{ marginTop: '10px', marginBottom: '5px', justifyContent: 'space-evenly', }} >
<IconButton disabled={!info.running} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="pause" size="small" title="Pause" onClick={handlePause}>
<PauseCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
Pause
</IconButton>
<IconButton disabled={info.running} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
aria-label="play" size="small" title="Play" onClick={handlePlay}>
<PlayCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
<IconButton disabled={info.running} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="play" size="small" title="Play" onClick={handlePlay}>
<PlayCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
{info.isPaused ? 'Resume' : 'Start'}
</IconButton>
<IconButton disabled={!info.running && !info.isPaused} sx={{display:'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' }}}
<IconButton disabled={!info.running && !info.isPaused} sx={{ display: 'grid', '&:hover': { color: '#1976d2', backgroundColor: 'transparent' } }}
aria-label="stop" size="small" title="Stop" onClick={handleStop}>
<StopCircle sx={{ fontSize: 30, justifySelf:'center' }}/>
<StopCircle sx={{ fontSize: 30, justifySelf: 'center' }} />
Stop
</IconButton>
<GenericModal onClose={() => {}} isOpen={decisionModal.open} canBeClosed={false}
modalStyle={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
background: 'white',
border: '2px solid #000',
boxShadow: '24',
height:'fit-content',
display:'block',
overflow:'scroll',
padding: '5px 25px 10px 25px',
}}>
<div style={{padding: '15px'}}>
<HelpIcon/>
<GenericModal onClose={() => { }} isOpen={decisionModal.open} canBeClosed={false}
modalStyle={{
position: 'absolute',
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: 500,
background: 'white',
border: '2px solid #000',
boxShadow: '24',
height: 'fit-content',
display: 'block',
overflow: 'scroll',
padding: '5px 25px 10px 25px',
}}>
<div style={{ padding: '15px' }}>
<HelpIcon />
{
handleDescription()
}
<div style={{float: 'right'}}>
<Button onClick={() => handleDecision(true)} color='success'>yes</Button>
<Button onClick={() => handleDecision(false)} color='error'>no</Button>
<div style={{ float: 'right' }}>
<Button onClick={() => handleDecision(true)} color='success'>yes</Button>
<Button onClick={() => handleDecision(false)} color='error'>no</Button>
</div>
</div>
</GenericModal>

View File

@@ -1,30 +1,57 @@
import * as React from 'react';
import Accordion from '@mui/material/Accordion';
import AccordionDetails from '@mui/material/AccordionDetails';
import AccordionSummary from '@mui/material/AccordionSummary';
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
import Typography from '@mui/material/Typography';
import ExpandMoreIcon from '@mui/icons-material/ExpandMore';
import Highlight from 'react-highlight'
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
import { Button, TextField } from '@mui/material';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import Highlight from 'react-highlight';
import { useCallback, useEffect, useRef, useState } from "react";
import { useSocketStore } from "../../context/socket";
import { useBrowserDimensionsStore } from "../../context/browserDimensions";
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TableRow from '@mui/material/TableRow';
import Paper from '@mui/material/Paper';
import StorageIcon from '@mui/icons-material/Storage';
export const InterpretationLog = () => {
const [expanded, setExpanded] = useState<boolean>(false);
interface InterpretationLogProps {
isOpen: boolean;
setIsOpen: (isOpen: boolean) => void;
}
export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, setIsOpen }) => {
const [log, setLog] = useState<string>('');
const [selectedOption, setSelectedOption] = useState<string>('10');
const [customValue, setCustomValue] = useState('');
const [tableData, setTableData] = useState<any[]>([]);
const logEndRef = useRef<HTMLDivElement | null>(null);
const handleChange = (isExpanded: boolean) => (event: React.SyntheticEvent) => {
setExpanded(isExpanded);
};
const { width } = useBrowserDimensionsStore();
const { socket } = useSocketStore();
const toggleDrawer = (newOpen: boolean) => (event: React.KeyboardEvent | React.MouseEvent) => {
if (
event.type === 'keydown' &&
((event as React.KeyboardEvent).key === 'Tab' ||
(event as React.KeyboardEvent).key === 'Shift')
) {
return;
}
setIsOpen(newOpen);
};
const scrollLogToBottom = () => {
if (logEndRef.current) {
logEndRef.current.scrollIntoView({ behavior: "smooth" })
logEndRef.current.scrollIntoView({ behavior: "smooth" });
}
}
};
const handleLog = useCallback((msg: string, date: boolean = true) => {
if (!date) {
@@ -33,14 +60,20 @@ export const InterpretationLog = () => {
setLog((prevState) => prevState + '\n' + `[${new Date().toLocaleString()}] ` + msg);
}
scrollLogToBottom();
}, [log, scrollLogToBottom])
}, [log, scrollLogToBottom]);
const handleSerializableCallback = useCallback((data: string) => {
const handleSerializableCallback = useCallback((data: any) => {
setLog((prevState) =>
prevState + '\n' + '---------- Serializable output data received ----------' + '\n'
+ JSON.stringify(data, null, 2) + '\n' + '--------------------------------------------------');
// Set table data
if (Array.isArray(data)) {
setTableData(data);
}
scrollLogToBottom();
}, [log, scrollLogToBottom])
}, [log, scrollLogToBottom]);
const handleBinaryCallback = useCallback(({ data, mimetype }: any) => {
setLog((prevState) =>
@@ -48,7 +81,15 @@ export const InterpretationLog = () => {
+ `mimetype: ${mimetype}` + '\n' + `data: ${JSON.stringify(data)}` + '\n'
+ '------------------------------------------------');
scrollLogToBottom();
}, [log, scrollLogToBottom])
}, [log, scrollLogToBottom]);
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedOption(event.target.value);
};
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setCustomValue(event.target.value);
};
useEffect(() => {
socket?.on('log', handleLog);
@@ -58,41 +99,113 @@ export const InterpretationLog = () => {
socket?.off('log', handleLog);
socket?.off('serializableCallback', handleSerializableCallback);
socket?.off('binaryCallback', handleBinaryCallback);
}
}, [socket, handleLog])
};
}, [socket, handleLog, handleSerializableCallback, handleBinaryCallback]);
// Extract columns dynamically from the first item of tableData
const columns = tableData.length > 0 ? Object.keys(tableData[0]) : [];
return (
<div>
<Accordion
expanded={expanded}
onChange={handleChange(!expanded)}
style={{ background: '#3f4853', color: 'white', borderRadius: '0px' }}
>
<AccordionSummary
expandIcon={<ExpandMoreIcon sx={{ color: 'white' }} />}
aria-controls="panel1bh-content"
id="panel1bh-header"
>
<Typography sx={{ width: '33%', flexShrink: 0 }}>
Interpretation Log
</Typography>
</AccordionSummary>
<AccordionDetails sx={{
background: '#19171c',
overflowY: 'scroll',
width: '100%',
aspectRatio: '4/1',
boxSizing: 'border-box',
<button
onClick={toggleDrawer(true)}
style={{
color: 'white',
background: '#3f4853',
border: 'none',
padding: '10px 20px',
width: 1280,
textAlign: 'left'
}}>
<div>
<Highlight className="javascript">
{log}
</Highlight>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
Interpretation Log
</button>
<SwipeableDrawer
anchor="bottom"
open={isOpen}
onClose={toggleDrawer(false)}
onOpen={toggleDrawer(true)}
PaperProps={{
sx: {
background: 'white',
color: 'black',
padding: '10px',
height: 720,
width: width - 10,
display: 'flex'
}
}}
>
<Typography variant="h6" gutterBottom>
<StorageIcon /> Output Data Preview
</Typography>
<div style={{
height: '50vh',
overflow: 'none',
padding: '10px',
}}>
{/* <Highlight className="javascript">
{log}
</Highlight> */}
{tableData.length > 0 && (
<TableContainer component={Paper}>
<Table sx={{ minWidth: 650 }} stickyHeader aria-label="output data table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell key={column}>{column}</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{tableData.map((row, index) => (
<TableRow key={index}>
{columns.map((column) => (
<TableCell key={column}>{row[column]}</TableCell>
))}
</TableRow>
))}
</TableBody>
</Table>
</TableContainer>
)}
<div style={{ display: 'flex', alignItems: 'flex-start', gap: '200px' }}>
<FormControl>
<FormLabel>
<h4>What is the maximum number of rows you want to extract?</h4>
</FormLabel>
<RadioGroup row value={selectedOption} onChange={handleRadioChange} sx={{ width: '500px' }}>
<FormControlLabel value="10" control={<Radio />} label="10" />
<FormControlLabel value="100" control={<Radio />} label="100" />
<FormControlLabel value="custom" control={<Radio />} label="Custom" />
{selectedOption === 'custom' && (
<TextField
type="number"
value={customValue}
onChange={handleCustomValueChange}
placeholder="Enter number"
sx={{
marginLeft: '10px',
marginTop: '-3px',
'& input': {
padding: '10px',
},
}}
/>
)}
</RadioGroup>
</FormControl>
<div style={{ paddingBottom: '40px' }}>
<h4>How can we find the next item?</h4>
<p>Select and review the pagination setting this webpage is using</p>
<Button variant="outlined">
Select Pagination Setting
</Button>
</div>
</div>
</AccordionDetails>
</Accordion>
<div style={{ float: "left", clear: "both" }}
ref={logEndRef} />
</div>
</SwipeableDrawer>
</div>
);
}

View File

@@ -123,7 +123,8 @@ export const BrowserContent = () => {
tabIndex={tabIndex}
/>
<BrowserNavBar
browserWidth={width - 10}
// todo: use width from browser dimension once fixed
browserWidth={1270}
handleUrlChanged={handleUrlChanged}
/>
<BrowserWindow/>

View File

@@ -50,17 +50,19 @@ const getAttributeOptions = (tagName: string, elementInfo: ElementInfo | null):
export const BrowserWindow = () => {
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; } | null>(null);
const [highlighterData, setHighlighterData] = useState<{ rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] } | null>(null);
const [showAttributeModal, setShowAttributeModal] = useState(false);
const [attributeOptions, setAttributeOptions] = useState<AttributeOption[]>([]);
const [selectedElement, setSelectedElement] = useState<{ selector: string, info: ElementInfo | null } | null>(null);
const [currentListId, setCurrentListId] = useState<number | null>(null);
const [listSelector, setListSelector] = useState<string | null>(null);
const [fields, setFields] = useState<Record<string, TextStep>>({});
const [paginationSelector, setPaginationSelector] = useState<string>('');
const { socket } = useSocketStore();
const { width, height } = useBrowserDimensionsStore();
const { getText, getList } = useActionContext();
const { getText, getList, paginationMode, paginationType } = useActionContext();
const { addTextStep, addListStep } = useBrowserSteps();
const onMouseMove = (e: MouseEvent) => {
@@ -78,6 +80,18 @@ export const BrowserWindow = () => {
}
};
const resetListState = useCallback(() => {
setListSelector(null);
setFields({});
setCurrentListId(null);
}, []);
useEffect(() => {
if (!getList) {
resetListState();
}
}, [getList, resetListState]);
const screencastHandler = useCallback((data: string) => {
setScreenShot(data);
}, [screenShot]);
@@ -96,12 +110,33 @@ export const BrowserWindow = () => {
}
}, [screenShot, canvasRef, socket, screencastHandler]);
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null }) => {
const highlighterHandler = useCallback((data: { rect: DOMRect, selector: string, elementInfo: ElementInfo | null, childSelectors?: string[] }) => {
if (getList === true) {
socket?.emit('setGetList', { getList: true });
if (listSelector) {
socket?.emit('listSelector', { selector: listSelector });
if (paginationMode) {
// Pagination mode: only set highlighterData if type is not empty, 'none', 'scrollDown', or 'scrollUp'
if (paginationType !== '' && paginationType !== 'scrollDown' && paginationType !== 'scrollUp' && paginationType !== 'none') {
setHighlighterData(data);
} else {
setHighlighterData(null);
}
} else if (data.childSelectors && data.childSelectors.includes(data.selector)) {
// !Pagination mode: highlight only valid child elements within the listSelector
setHighlighterData(data);
} else {
// If not a valid child in normal mode, clear the highlighter
setHighlighterData(null);
}
} else {
setHighlighterData(data); // Set highlighterData for the initial listSelector selection
}
} else {
setHighlighterData(data); // For non-list steps
}
setHighlighterData(data);
}, [highlighterData, getList, socket]);
}, [highlighterData, getList, socket, listSelector, paginationMode, paginationType]);
useEffect(() => {
document.addEventListener('mousemove', onMouseMove, false);
@@ -127,6 +162,7 @@ export const BrowserWindow = () => {
clickY >= highlightRect.top &&
clickY <= highlightRect.bottom
) {
const options = getAttributeOptions(highlighterData.elementInfo?.tagName || '', highlighterData.elementInfo);
if (getText === true) {
@@ -153,17 +189,32 @@ export const BrowserWindow = () => {
}
}
if (paginationMode && getList) {
// 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);
addListStep(listSelector!, fields, currentListId || 0, { type: paginationType, selector: highlighterData.selector });
}
return;
}
if (getList === true && !listSelector) {
setListSelector(highlighterData.selector);
} else if (getList === true && listSelector) {
setCurrentListId(Date.now());
setFields({});
} else if (getList === true && listSelector && currentListId) {
const attribute = options[0].value;
const data = attribute === 'href' ? highlighterData.elementInfo?.url || '' :
attribute === 'src' ? highlighterData.elementInfo?.imageUrl || '' :
highlighterData.elementInfo?.innerText || '';
// Add fields to the list
if (options.length === 1) {
// Handle directly without showing the modal
const attribute = options[0].value;
const newField: TextStep = {
id: Date.now(),
type: 'text',
label: `Label ${Object.keys(fields).length + 1}`,
data: highlighterData.elementInfo?.innerText || '',
data: data,
selectorObj: {
selector: highlighterData.selector,
tag: highlighterData.elementInfo?.tagName,
@@ -176,14 +227,15 @@ export const BrowserWindow = () => {
...prevFields,
[newField.label]: newField
};
console.log(updatedFields)
return updatedFields;
});
if (listSelector) {
addListStep(listSelector, { ...fields, [newField.label]: newField });
addListStep(listSelector, { ...fields, [newField.label]: newField }, currentListId, { type: '', selector: paginationSelector });
}
} else {
// Show the modal if there are multiple options
setAttributeOptions(options);
setSelectedElement({
selector: highlighterData.selector,
@@ -217,12 +269,12 @@ export const BrowserWindow = () => {
attribute: attribute
});
}
if (getList === true) {
if (getList === true && listSelector && currentListId) {
const newField: TextStep = {
id: Date.now(),
type: 'text',
label: `Label ${Object.keys(fields).length + 1}`,
data: selectedElement.info?.innerText || '',
data: data,
selectorObj: {
selector: selectedElement.selector,
tag: selectedElement.info?.tagName,
@@ -235,18 +287,32 @@ export const BrowserWindow = () => {
...prevFields,
[newField.label]: newField
};
console.log(updatedFields)
return updatedFields;
});
if (listSelector) {
addListStep(listSelector, { ...fields, [newField.label]: newField });
addListStep(listSelector, { ...fields, [newField.label]: newField }, currentListId, { type: '', selector: paginationSelector });
}
}
}
}
setShowAttributeModal(false);
};
const resetPaginationSelector = useCallback(() => {
setPaginationSelector('');
}, []);
useEffect(() => {
if (!paginationMode) {
resetPaginationSelector();
}
}, [paginationMode, resetPaginationSelector]);
return (
<div onClick={handleClick}>
{

View File

@@ -14,21 +14,21 @@ import { RunSettings } from "../molecules/RunSettings";
const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => {
getActiveWorkflow(id).then(
(response ) => {
if (response){
(response) => {
if (response) {
callback(response);
} else {
throw new Error("No workflow found");
}
}
).catch((error) => {console.log(error.message)})
).catch((error) => { console.log(error.message) })
};
interface LeftSidePanelProps {
sidePanelRef: HTMLDivElement | null;
alreadyHasScrollbar: boolean;
recordingName: string;
handleSelectPairForEdit: (pair:WhereWhatPair, index:number) => void;
handleSelectPairForEdit: (pair: WhereWhatPair, index: number) => void;
}
export const LeftSidePanel = (
@@ -59,10 +59,11 @@ export const LeftSidePanel = (
fetchWorkflow(id, workflowHandler);
}
// fetch workflow in 15min intervals
let interval = setInterval(() =>{
if (id) {
fetchWorkflow(id, workflowHandler);
}}, (1000 * 60 * 15));
let interval = setInterval(() => {
if (id) {
fetchWorkflow(id, workflowHandler);
}
}, (1000 * 60 * 15));
return () => clearInterval(interval)
}, [id]);
@@ -104,19 +105,19 @@ export const LeftSidePanel = (
flexDirection: 'column',
}}
>
<SidePanelHeader/>
<SidePanelHeader />
<TabContext value={tab}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)}>
<Tab label="Recording" value='recording' />
<Tab label="Settings" value='settings' onClick={() => {
getParamsOfActiveWorkflow(id).then((response) => {
if (response) {
setParams(response);
}
})
}}/>
</Tabs>
<TabPanel value='recording' sx={{padding: '0px'}}>
<Tabs value={tab} onChange={(e, newTab) => setTab(newTab)}>
<Tab label="Recording" value='recording' />
<Tab label="Settings" value='settings' onClick={() => {
getParamsOfActiveWorkflow(id).then((response) => {
if (response) {
setParams(response);
}
})
}} />
</Tabs>
<TabPanel value='recording' sx={{ padding: '0px' }}>
<LeftSidePanelContent
workflow={workflow}
updateWorkflow={setWorkflow}
@@ -126,7 +127,7 @@ export const LeftSidePanel = (
</TabPanel>
<TabPanel value='settings'>
<LeftSidePanelSettings params={params}
settings={settings} setSettings={setSettings}/>
settings={settings} setSettings={setSettings} />
</TabPanel>
</TabContext>
</Paper>

View File

@@ -3,29 +3,40 @@ import { Button, Paper, Box, TextField } from "@mui/material";
import EditIcon from '@mui/icons-material/Edit';
import TextFieldsIcon from '@mui/icons-material/TextFields';
import DocumentScannerIcon from '@mui/icons-material/DocumentScanner';
import styled from "styled-components";
import { SimpleBox } from "../atoms/Box";
import Typography from "@mui/material/Typography";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { useActionContext } from '../../context/browserActions';
import { useBrowserSteps, ListStep, TextStep, SelectorObject } from '../../context/browserSteps';
import { PaginationType, useActionContext, LimitType } from '../../context/browserActions';
import { useBrowserSteps } from '../../context/browserSteps';
import { useSocketStore } from '../../context/socket';
import { ScreenshotSettings } from '../../shared/types';
import InputAdornment from '@mui/material/InputAdornment';
import { SidePanelHeader } from '../molecules/SidePanelHeader';
import FormControlLabel from '@mui/material/FormControlLabel';
import FormControl from '@mui/material/FormControl';
import FormLabel from '@mui/material/FormLabel';
import Radio from '@mui/material/Radio';
import RadioGroup from '@mui/material/RadioGroup';
// TODO:
// 1. Handle field label update
// 2. Handle field deletion | confirmation
// 3. Add description for each browser step
// 4. Handle non custom action steps
interface RightSidePanelProps {
onFinishCapture: () => void;
}
export const RightSidePanel = () => {
export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture }) => {
const [textLabels, setTextLabels] = useState<{ [id: number]: string }>({});
const [errors, setErrors] = useState<{ [id: number]: string }>({});
const [confirmedTextSteps, setConfirmedTextSteps] = useState<{ [id: number]: boolean }>({});
const [showPaginationOptions, setShowPaginationOptions] = useState(false);
const [showLimitOptions, setShowLimitOptions] = useState(false);
const [captureStage, setCaptureStage] = useState<'initial' | 'pagination' | 'limit' | 'complete'>('initial');
const { lastAction, notify } = useGlobalInfoStore();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, getList, startGetList, stopGetList } = useActionContext();
const { getText, startGetText, stopGetText, getScreenshot, startGetScreenshot, stopGetScreenshot, paginationMode, getList, startGetList, stopGetList, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitMode, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode } = useActionContext();
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep } = useBrowserSteps();
const { socket } = useSocketStore();
@@ -83,11 +94,17 @@ export const RightSidePanel = () => {
if (hasTextSteps) {
socket?.emit('action', { action: 'scrapeSchema', settings });
}
onFinishCapture();
}, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps]);
const getListSettingsObject = useCallback(() => {
let settings: { listSelector?: string; fields?: Record<string, { selector: string; tag?: string;[key: string]: any }> } = {};
let settings: {
listSelector?: string;
fields?: Record<string, { selector: string; tag?: string;[key: string]: any }>;
pagination?: { type: string; selector?: string };
limit?: number;
} = {};
browserSteps.forEach(step => {
if (step.type === 'list' && step.listSelector && Object.keys(step.fields).length > 0) {
@@ -97,36 +114,101 @@ export const RightSidePanel = () => {
fields[label] = {
selector: field.selectorObj.selector,
tag: field.selectorObj.tag,
attribute: field.selectorObj.attribute
attribute: field.selectorObj.attribute,
};
}
});
settings = {
listSelector: step.listSelector,
fields: fields
fields: fields,
pagination: { type: paginationType, selector: step.pagination?.selector },
limit: parseInt(limitType === 'custom' ? customLimit : limitType),
};
}
});
return settings;
}, [browserSteps]);
}, [browserSteps, paginationType, limitType, customLimit]);
const resetListState = useCallback(() => {
setShowPaginationOptions(false);
updatePaginationType('');
setShowLimitOptions(false);
updateLimitType('');
updateCustomLimit('');
}, [updatePaginationType, updateLimitType, updateCustomLimit]);
const handleStopGetList = useCallback(() => {
stopGetList();
resetListState();
}, [stopGetList, resetListState]);
const stopCaptureAndEmitGetListSettings = useCallback(() => {
stopGetList();
const settings = getListSettingsObject();
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.');
}
}, [stopGetList, getListSettingsObject, socket, notify]);
handleStopGetList();
onFinishCapture();
}, [stopGetList, getListSettingsObject, socket, notify, handleStopGetList]);
// const handleListFieldChange = (stepId: number, key: 'label' | 'data', value: string) => {
// updateListStepField(stepId, key, value);
// };
const handleConfirmListCapture = useCallback(() => {
switch (captureStage) {
case 'initial':
startPaginationMode();
setShowPaginationOptions(true);
setCaptureStage('pagination');
break;
case 'pagination':
if (!paginationType) {
notify('error', 'Please select a pagination type.');
return;
}
const settings = getListSettingsObject();
const paginationSelector = settings.pagination?.selector;
if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) {
notify('error', 'Please select the pagination element first.');
return;
}
stopPaginationMode();
setShowPaginationOptions(false);
startLimitMode();
setShowLimitOptions(true);
setCaptureStage('limit');
break;
case 'limit':
if (!limitType || (limitType === 'custom' && !customLimit)) {
notify('error', 'Please select a limit or enter a custom limit.');
return;
}
const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType);
if (isNaN(limit) || limit <= 0) {
notify('error', 'Please enter a valid limit.');
return;
}
stopLimitMode();
setShowLimitOptions(false);
stopCaptureAndEmitGetListSettings();
setCaptureStage('complete');
break;
case 'complete':
setCaptureStage('initial');
break;
}
}, [captureStage, paginationType, limitType, customLimit, startPaginationMode, stopPaginationMode, startLimitMode, stopLimitMode, notify, stopCaptureAndEmitGetListSettings, getListSettingsObject]);
const handlePaginationSettingSelect = (option: PaginationType) => {
updatePaginationType(option);
if (['clickNext', 'clickLoadMore'].includes(option)) {
}
};
const captureScreenshot = (fullPage: boolean) => {
const screenshotSettings: ScreenshotSettings = {
@@ -147,28 +229,76 @@ export const RightSidePanel = () => {
<SimpleBox height={60} width='100%' background='lightGray' radius='0%'>
<Typography sx={{ padding: '10px' }}>Last action: {` ${lastAction}`}</Typography>
</SimpleBox>
<SidePanelHeader />
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '15px' }}>
{!getText && !getScreenshot && !getList && <Button variant="contained" onClick={startGetList}>Capture List</Button>}
{getList &&
{getList && (
<>
<Box display="flex" justifyContent="space-between" gap={2} style={{ margin: '15px' }}>
<Button variant="outlined" onClick={stopCaptureAndEmitGetListSettings}>Confirm</Button>
<Button variant="outlined" color="error" onClick={stopGetList}>Discard</Button>
<Button variant="outlined" onClick={handleConfirmListCapture}>
{captureStage === 'initial' ? 'Confirm Capture' :
captureStage === 'pagination' ? 'Confirm Pagination' :
captureStage === 'limit' ? 'Confirm Limit' : 'Finish Capture'}
</Button>
<Button variant="outlined" color="error" onClick={handleStopGetList}>Discard</Button>
</Box>
</>
}
)}
{showPaginationOptions && (
<Box display="flex" flexDirection="column" gap={2} style={{ margin: '15px' }}>
<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>
</Box>
)}
{showLimitOptions && (
<FormControl>
<FormLabel>
<h4>What is the maximum number of rows you want to extract?</h4>
</FormLabel>
<RadioGroup
value={limitType}
onChange={(e) => updateLimitType(e.target.value as LimitType)}
sx={{
display: 'flex',
flexDirection: 'column',
width: '500px'
}}
>
<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" />
{limitType === 'custom' && (
<TextField
type="number"
value={customLimit}
onChange={(e) => updateCustomLimit(e.target.value)}
placeholder="Enter number"
sx={{
marginLeft: '10px',
'& input': {
padding: '10px',
},
}}
/>
)}
</div>
</RadioGroup>
</FormControl>
)}
{!getText && !getScreenshot && !getList && <Button variant="contained" onClick={startGetText}>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={stopGetText}>Discard</Button>
<Button variant="outlined" onClick={stopCaptureAndEmitGetTextSettings} >Confirm</Button>
<Button variant="outlined" color="error" onClick={stopGetText} >Discard</Button>
</Box>
</>
}
{!getText && !getScreenshot && !getList && <Button variant="contained" onClick={startGetScreenshot}>Capture Screenshot</Button>}
{getScreenshot && (
<Box display="flex" flexDirection="column" gap={2}>
@@ -178,7 +308,6 @@ export const RightSidePanel = () => {
</Box>
)}
</Box>
<Box>
{browserSteps.map(step => (
<Box key={step.id} sx={{ boxShadow: 5, padding: '10px', margin: '10px', borderRadius: '4px' }}>

View File

@@ -1,15 +1,30 @@
import React, { createContext, useContext, useState, ReactNode } from 'react';
export type PaginationType = 'scrollDown' | 'scrollUp' | 'clickNext' | 'clickLoadMore' | 'none' | '';
export type LimitType = '10' | '100' | 'custom' | '';
interface ActionContextProps {
getText: boolean;
getList: boolean;
getScreenshot: boolean;
paginationMode: boolean;
limitMode: boolean;
paginationType: PaginationType;
limitType: LimitType;
customLimit: string;
startPaginationMode: () => void;
startGetText: () => void;
stopGetText: () => void;
startGetList: () => void;
stopGetList: () => void;
startGetScreenshot: () => void;
stopGetScreenshot: () => void;
stopPaginationMode: () => void;
updatePaginationType: (type: PaginationType) => void;
startLimitMode: () => void;
stopLimitMode: () => void;
updateLimitType: (type: LimitType) => void;
updateCustomLimit: (limit: string) => void;
}
const ActionContext = createContext<ActionContextProps | undefined>(undefined);
@@ -18,18 +33,60 @@ export const ActionProvider = ({ children }: { children: ReactNode }) => {
const [getText, setGetText] = useState<boolean>(false);
const [getList, setGetList] = useState<boolean>(false);
const [getScreenshot, setGetScreenshot] = useState<boolean>(false);
const [paginationMode, setPaginationMode] = useState<boolean>(false);
const [limitMode, setLimitMode] = useState<boolean>(false);
const [paginationType, setPaginationType] = useState<PaginationType>('');
const [limitType, setLimitType] = useState<LimitType>('');
const [customLimit, setCustomLimit] = useState<string>('');
const updatePaginationType = (type: PaginationType) => setPaginationType(type);
const updateLimitType = (type: LimitType) => setLimitType(type);
const updateCustomLimit = (limit: string) => setCustomLimit(limit);
const startPaginationMode = () => setPaginationMode(true);
const stopPaginationMode = () => setPaginationMode(false);
const startLimitMode = () => setLimitMode(true);
const stopLimitMode = () => setLimitMode(false);
const startGetText = () => setGetText(true);
const stopGetText = () => setGetText(false);
const startGetList = () => setGetList(true);
const stopGetList = () => setGetList(false);
const stopGetList = () => {
setGetList(false);
setPaginationType('');
setLimitType('');
setCustomLimit('');
};
const startGetScreenshot = () => setGetScreenshot(true);
const stopGetScreenshot = () => setGetScreenshot(false);
return (
<ActionContext.Provider value={{ getText, getList, getScreenshot, startGetText, stopGetText, startGetList, stopGetList, startGetScreenshot, stopGetScreenshot }}>
<ActionContext.Provider value={{
getText,
getList,
getScreenshot,
paginationMode,
limitMode,
paginationType,
limitType,
customLimit,
startGetText,
stopGetText,
startGetList,
stopGetList,
startGetScreenshot,
stopGetScreenshot,
startPaginationMode,
stopPaginationMode,
startLimitMode,
stopLimitMode,
updatePaginationType,
updateLimitType,
updateCustomLimit
}}>
{children}
</ActionContext.Provider>
);
@@ -41,4 +98,4 @@ export const useActionContext = () => {
throw new Error('useActionContext must be used within an ActionProvider');
}
return context;
};
};

View File

@@ -19,6 +19,11 @@ export interface ListStep {
type: 'list';
listSelector: string;
fields: { [key: string]: TextStep };
pagination?: {
type: string;
selector: string;
};
limit?: number;
}
type BrowserStep = TextStep | ScreenshotStep | ListStep;
@@ -33,7 +38,7 @@ export interface SelectorObject {
interface BrowserStepsContextType {
browserSteps: BrowserStep[];
addTextStep: (label: string, data: string, selectorObj: SelectorObject) => void;
addListStep: (listSelector: string, fields: { [key: string]: TextStep }) => void
addListStep: (listSelector: string, fields: { [key: string]: TextStep }, listId: number, pagination?: { type: string; selector: string }, limit?: number) => void
addScreenshotStep: (fullPage: boolean) => void;
deleteBrowserStep: (id: number) => void;
updateBrowserTextStepLabel: (id: number, newLabel: string) => void;
@@ -51,10 +56,10 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
]);
};
const addListStep = (listSelector: string, newFields: { [key: string]: TextStep }) => {
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.listSelector === listSelector
step => step.type === 'list' && step.id === listId
);
if (existingListStepIndex !== -1) {
// Update the existing ListStep with new fields
@@ -62,20 +67,21 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
const existingListStep = updatedSteps[existingListStepIndex] as ListStep;
updatedSteps[existingListStepIndex] = {
...existingListStep,
fields: { ...existingListStep.fields, ...newFields }
fields: { ...existingListStep.fields, ...newFields },
pagination: pagination,
limit: limit,
};
return updatedSteps;
} else {
// Create a new ListStep
return [
...prevSteps,
{ id: Date.now(), type: 'list', listSelector, fields: newFields }
{ id: listId, type: 'list', listSelector, fields: newFields, pagination, limit }
];
}
});
};
const addScreenshotStep = (fullPage: boolean) => {
setBrowserSteps(prevSteps => [
...prevSteps,

View File

@@ -7,11 +7,8 @@ import { MainPage } from "./MainPage";
import { useGlobalInfoStore } from "../context/globalInfo";
import { getActiveBrowserId } from "../api/recording";
import { AlertSnackbar } from "../components/atoms/AlertSnackbar";
import { InterpretationLog } from "../components/molecules/InterpretationLog";
export const PageWrapper = () => {
const [recordingName, setRecordingName] = useState('');
const [open, setOpen] = useState(false);
@@ -54,7 +51,6 @@ export const PageWrapper = () => {
<BrowserDimensionsProvider>
<React.Fragment>
<RecordingPage recordingName={recordingName} />
<InterpretationLog />
</React.Fragment>
</BrowserDimensionsProvider>
)

View File

@@ -32,6 +32,7 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
pair: null,
index: 0,
});
const [showOutputData, setShowOutputData] = useState(false);
const browserContentRef = React.useRef<HTMLDivElement>(null);
const workflowListRef = React.useRef<HTMLDivElement>(null);
@@ -40,6 +41,10 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
const { setWidth } = useBrowserDimensionsStore();
const { browserId, setBrowserId } = useGlobalInfoStore();
const handleShowOutputData = useCallback(() => {
setShowOutputData(true);
}, []);
const handleSelectPairForEdit = (pair: WhereWhatPair, index: number) => {
setPairForEdit({
pair,
@@ -47,7 +52,6 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
});
};
//resize browser content when loaded event is fired
useEffect(() => changeBrowserDimensions(), [isLoaded])
useEffect(() => {
@@ -122,10 +126,10 @@ export const RecordingPage = ({ recordingName }: RecordingPageProps) => {
</Grid>
<Grid id="browser-content" ref={browserContentRef} item xs>
<BrowserContent />
<InterpretationLog />
<InterpretationLog isOpen={showOutputData} setIsOpen={setShowOutputData} />
</Grid>
<Grid item xs={2}>
<RightSidePanel />
<RightSidePanel onFinishCapture={handleShowOutputData} />
</Grid>
</Grid>
: <Loader />}