Merge pull request #583 from getmaxun/faster-preview
feat: faster output preview
This commit is contained in:
@@ -37,6 +37,7 @@ declare global {
|
|||||||
* Defines optional intepreter options (passed in constructor)
|
* Defines optional intepreter options (passed in constructor)
|
||||||
*/
|
*/
|
||||||
interface InterpreterOptions {
|
interface InterpreterOptions {
|
||||||
|
mode?: string;
|
||||||
maxRepeats: number;
|
maxRepeats: number;
|
||||||
maxConcurrency: number;
|
maxConcurrency: number;
|
||||||
serializableCallback: (output: any) => (void | Promise<void>);
|
serializableCallback: (output: any) => (void | Promise<void>);
|
||||||
@@ -432,6 +433,11 @@ export default class Interpreter extends EventEmitter {
|
|||||||
if (this.options.debugChannel?.setActionType) {
|
if (this.options.debugChannel?.setActionType) {
|
||||||
this.options.debugChannel.setActionType('scrapeSchema');
|
this.options.debugChannel.setActionType('scrapeSchema');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.mode && this.options.mode === 'editor') {
|
||||||
|
await this.options.serializableCallback({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureScriptsLoaded(page);
|
await this.ensureScriptsLoaded(page);
|
||||||
|
|
||||||
@@ -463,6 +469,11 @@ export default class Interpreter extends EventEmitter {
|
|||||||
this.options.debugChannel.setActionType('scrapeList');
|
this.options.debugChannel.setActionType('scrapeList');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (this.options.mode && this.options.mode === 'editor') {
|
||||||
|
await this.options.serializableCallback({});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
await this.ensureScriptsLoaded(page);
|
await this.ensureScriptsLoaded(page);
|
||||||
if (!config.pagination) {
|
if (!config.pagination) {
|
||||||
const scrapeResults: Record<string, any>[] = await page.evaluate((cfg) => window.scrapeList(cfg), config);
|
const scrapeResults: Record<string, any>[] = await page.evaluate((cfg) => window.scrapeList(cfg), config);
|
||||||
|
|||||||
@@ -427,6 +427,541 @@ export class RemoteBrowser {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Extract data from a list of elements on a page
|
||||||
|
* @param page - Playwright Page object
|
||||||
|
* @param listSelector - CSS selector for the list container
|
||||||
|
* @param fields - Record of field configurations
|
||||||
|
* @param limit - Maximum number of items to extract (default: 5)
|
||||||
|
* @returns Promise<Array<Record<string, string>>> - Array of extracted data objects
|
||||||
|
*/
|
||||||
|
private async extractListData(
|
||||||
|
page: Page,
|
||||||
|
listSelector: string,
|
||||||
|
fields: Record<string, {
|
||||||
|
label: string;
|
||||||
|
selectorObj: {
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
}>,
|
||||||
|
limit: number = 5
|
||||||
|
): Promise<Array<Record<string, string>>> {
|
||||||
|
return await page.evaluate(
|
||||||
|
async ({ listSelector, fields, limit }: {
|
||||||
|
listSelector: string;
|
||||||
|
fields: Record<string, {
|
||||||
|
label: string;
|
||||||
|
selectorObj: {
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
};
|
||||||
|
}>;
|
||||||
|
limit: number;
|
||||||
|
}) => {
|
||||||
|
const convertedFields: Record<string, {
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
}> = {};
|
||||||
|
|
||||||
|
for (const [key, field] of Object.entries(fields)) {
|
||||||
|
convertedFields[field.label] = {
|
||||||
|
selector: field.selectorObj.selector,
|
||||||
|
attribute: field.selectorObj.attribute
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
const queryElement = (rootElement: Element | Document, selector: string): Element | null => {
|
||||||
|
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||||
|
return rootElement.querySelector(selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||||
|
let currentElement: Element | Document | null = rootElement;
|
||||||
|
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
if (!currentElement) return null;
|
||||||
|
|
||||||
|
if ((currentElement as Element).tagName === 'IFRAME' || (currentElement as Element).tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = currentElement as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
const frameDoc = frameElement.contentDocument || frameElement.contentWindow?.document;
|
||||||
|
if (!frameDoc) return null;
|
||||||
|
currentElement = frameDoc.querySelector(parts[i]);
|
||||||
|
continue;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Cannot access ${(currentElement as Element).tagName.toLowerCase()} content:`, e);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let nextElement: Element | null = null;
|
||||||
|
|
||||||
|
if ('querySelector' in currentElement) {
|
||||||
|
nextElement = currentElement.querySelector(parts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextElement && 'shadowRoot' in currentElement && (currentElement as Element).shadowRoot) {
|
||||||
|
nextElement = (currentElement as Element).shadowRoot!.querySelector(parts[i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!nextElement && 'children' in currentElement) {
|
||||||
|
const children: any = Array.from((currentElement as Element).children || []);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.shadowRoot) {
|
||||||
|
nextElement = child.shadowRoot.querySelector(parts[i]);
|
||||||
|
if (nextElement) break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElement = nextElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentElement as Element | null;
|
||||||
|
};
|
||||||
|
|
||||||
|
const queryElementAll = (rootElement: Element | Document, selector: string): Element[] => {
|
||||||
|
if (!selector.includes('>>') && !selector.includes(':>>')) {
|
||||||
|
return Array.from(rootElement.querySelectorAll(selector));
|
||||||
|
}
|
||||||
|
|
||||||
|
const parts = selector.split(/(?:>>|:>>)/).map(part => part.trim());
|
||||||
|
let currentElements: (Element | Document)[] = [rootElement];
|
||||||
|
|
||||||
|
for (const part of parts) {
|
||||||
|
const nextElements: Element[] = [];
|
||||||
|
|
||||||
|
for (const element of currentElements) {
|
||||||
|
if ((element as Element).tagName === 'IFRAME' || (element as Element).tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = element as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
const frameDoc = frameElement.contentDocument || frameElement.contentWindow?.document;
|
||||||
|
if (frameDoc) {
|
||||||
|
nextElements.push(...Array.from(frameDoc.querySelectorAll(part)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Cannot access ${(element as Element).tagName.toLowerCase()} content:`, e);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if ('querySelectorAll' in element) {
|
||||||
|
nextElements.push(...Array.from(element.querySelectorAll(part)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('shadowRoot' in element && (element as Element).shadowRoot) {
|
||||||
|
nextElements.push(...Array.from((element as Element).shadowRoot!.querySelectorAll(part)));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ('children' in element) {
|
||||||
|
const children = Array.from((element as Element).children || []);
|
||||||
|
for (const child of children) {
|
||||||
|
if (child.shadowRoot) {
|
||||||
|
nextElements.push(...Array.from(child.shadowRoot.querySelectorAll(part)));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
currentElements = nextElements;
|
||||||
|
}
|
||||||
|
|
||||||
|
return currentElements as Element[];
|
||||||
|
};
|
||||||
|
|
||||||
|
function extractValue(element: Element, attribute: string): string | null {
|
||||||
|
if (!element) return null;
|
||||||
|
|
||||||
|
const baseURL = element.ownerDocument?.location?.href || window.location.origin;
|
||||||
|
|
||||||
|
if (element.shadowRoot) {
|
||||||
|
const shadowContent = element.shadowRoot.textContent;
|
||||||
|
if (shadowContent?.trim()) {
|
||||||
|
return shadowContent.trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attribute === 'innerText') {
|
||||||
|
return (element as HTMLElement).innerText.trim();
|
||||||
|
} else if (attribute === 'innerHTML') {
|
||||||
|
return element.innerHTML.trim();
|
||||||
|
} else if (attribute === 'src' || attribute === 'href') {
|
||||||
|
if (attribute === 'href' && element.tagName !== 'A') {
|
||||||
|
const parentElement = element.parentElement;
|
||||||
|
if (parentElement && parentElement.tagName === 'A') {
|
||||||
|
const parentHref = parentElement.getAttribute('href');
|
||||||
|
if (parentHref) {
|
||||||
|
try {
|
||||||
|
return new URL(parentHref, baseURL).href;
|
||||||
|
} catch (e) {
|
||||||
|
return parentHref;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const attrValue = element.getAttribute(attribute);
|
||||||
|
const dataAttr = attrValue || element.getAttribute('data-' + attribute);
|
||||||
|
|
||||||
|
if (!dataAttr || dataAttr.trim() === '') {
|
||||||
|
if (attribute === 'src') {
|
||||||
|
const style = window.getComputedStyle(element);
|
||||||
|
const bgImage = style.backgroundImage;
|
||||||
|
if (bgImage && bgImage !== 'none') {
|
||||||
|
const matches = bgImage.match(/url\(['"]?([^'")]+)['"]?\)/);
|
||||||
|
return matches ? new URL(matches[1], baseURL).href : null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
return new URL(dataAttr, baseURL).href;
|
||||||
|
} catch (e) {
|
||||||
|
console.warn('Error creating URL from', dataAttr, e);
|
||||||
|
return dataAttr; // Return the original value if URL construction fails
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return element.getAttribute(attribute);
|
||||||
|
}
|
||||||
|
|
||||||
|
function findTableAncestor(element: Element): { type: string; element: Element } | null {
|
||||||
|
let currentElement: Element | null = element;
|
||||||
|
const MAX_DEPTH = 5;
|
||||||
|
let depth = 0;
|
||||||
|
|
||||||
|
while (currentElement && depth < MAX_DEPTH) {
|
||||||
|
if (currentElement.getRootNode() instanceof ShadowRoot) {
|
||||||
|
currentElement = (currentElement.getRootNode() as ShadowRoot).host;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentElement.tagName === 'TD') {
|
||||||
|
return { type: 'TD', element: currentElement };
|
||||||
|
} else if (currentElement.tagName === 'TR') {
|
||||||
|
return { type: 'TR', element: currentElement };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentElement.tagName === 'IFRAME' || currentElement.tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = currentElement as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
currentElement = frameElement.contentDocument?.body || null;
|
||||||
|
} catch (e) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
currentElement = currentElement.parentElement;
|
||||||
|
}
|
||||||
|
depth++;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCellIndex(td: Element): number {
|
||||||
|
if (td.getRootNode() instanceof ShadowRoot) {
|
||||||
|
const shadowRoot = td.getRootNode() as ShadowRoot;
|
||||||
|
const allCells = Array.from(shadowRoot.querySelectorAll('td'));
|
||||||
|
return allCells.indexOf(td as HTMLTableCellElement);
|
||||||
|
}
|
||||||
|
|
||||||
|
let index = 0;
|
||||||
|
let sibling = td;
|
||||||
|
while (sibling = sibling.previousElementSibling as Element) {
|
||||||
|
index++;
|
||||||
|
}
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
|
||||||
|
function hasThElement(row: Element, tableFields: Record<string, { selector: string; attribute: string }>): boolean {
|
||||||
|
for (const [_, { selector }] of Object.entries(tableFields)) {
|
||||||
|
const element = queryElement(row, selector);
|
||||||
|
if (element) {
|
||||||
|
let current: Element | ShadowRoot | Document | null = element;
|
||||||
|
while (current && current !== row) {
|
||||||
|
if (current.getRootNode() instanceof ShadowRoot) {
|
||||||
|
current = (current.getRootNode() as ShadowRoot).host;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((current as Element).tagName === 'TH') return true;
|
||||||
|
|
||||||
|
if ((current as Element).tagName === 'IFRAME' || (current as Element).tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = current as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
current = frameElement.contentDocument?.body || null;
|
||||||
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
current = (current as Element).parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
function filterRowsBasedOnTag(rows: Element[], tableFields: Record<string, { selector: string; attribute: string }>): Element[] {
|
||||||
|
for (const row of rows) {
|
||||||
|
if (hasThElement(row, tableFields)) {
|
||||||
|
return rows;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return rows.filter(row => {
|
||||||
|
const directTH = row.getElementsByTagName('TH').length === 0;
|
||||||
|
const shadowTH = row.shadowRoot ?
|
||||||
|
row.shadowRoot.querySelector('th') === null : true;
|
||||||
|
return directTH && shadowTH;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function calculateClassSimilarity(classList1: string[], classList2: string[]): number {
|
||||||
|
const set1 = new Set(classList1);
|
||||||
|
const set2 = new Set(classList2);
|
||||||
|
const intersection = new Set([...set1].filter(x => set2.has(x)));
|
||||||
|
const union = new Set([...set1, ...set2]);
|
||||||
|
return intersection.size / union.size;
|
||||||
|
}
|
||||||
|
|
||||||
|
function findSimilarElements(baseElement: Element, similarityThreshold: number = 0.7): Element[] {
|
||||||
|
const baseClasses = Array.from(baseElement.classList);
|
||||||
|
if (baseClasses.length === 0) return [];
|
||||||
|
|
||||||
|
const allElements: Element[] = [];
|
||||||
|
|
||||||
|
allElements.push(...Array.from(document.getElementsByTagName(baseElement.tagName)));
|
||||||
|
|
||||||
|
if (baseElement.getRootNode() instanceof ShadowRoot) {
|
||||||
|
const shadowHost = (baseElement.getRootNode() as ShadowRoot).host;
|
||||||
|
allElements.push(...Array.from(shadowHost.getElementsByTagName(baseElement.tagName)));
|
||||||
|
}
|
||||||
|
|
||||||
|
const frames = [
|
||||||
|
...Array.from(document.getElementsByTagName('iframe')),
|
||||||
|
...Array.from(document.getElementsByTagName('frame'))
|
||||||
|
];
|
||||||
|
|
||||||
|
for (const frame of frames) {
|
||||||
|
try {
|
||||||
|
const frameElement = frame as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
const frameDoc = frameElement.contentDocument || frameElement.contentWindow?.document;
|
||||||
|
if (frameDoc) {
|
||||||
|
allElements.push(...Array.from(frameDoc.getElementsByTagName(baseElement.tagName)));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Cannot access ${frame.tagName.toLowerCase()} content:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return allElements.filter(element => {
|
||||||
|
if (element === baseElement) return false;
|
||||||
|
const similarity = calculateClassSimilarity(
|
||||||
|
baseClasses,
|
||||||
|
Array.from(element.classList)
|
||||||
|
);
|
||||||
|
return similarity >= similarityThreshold;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
let containers = queryElementAll(document, listSelector);
|
||||||
|
|
||||||
|
if (containers.length === 0) return [];
|
||||||
|
|
||||||
|
if (limit > 1 && containers.length === 1) {
|
||||||
|
const baseContainer = containers[0];
|
||||||
|
const similarContainers = findSimilarElements(baseContainer);
|
||||||
|
|
||||||
|
if (similarContainers.length > 0) {
|
||||||
|
const newContainers = similarContainers.filter(container =>
|
||||||
|
!container.matches(listSelector)
|
||||||
|
);
|
||||||
|
containers = [...containers, ...newContainers];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const containerFields = containers.map(() => ({
|
||||||
|
tableFields: {} as Record<string, {
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
tableContext?: string;
|
||||||
|
cellIndex?: number;
|
||||||
|
}>,
|
||||||
|
nonTableFields: {} as Record<string, {
|
||||||
|
selector: string;
|
||||||
|
attribute: string;
|
||||||
|
}>
|
||||||
|
}));
|
||||||
|
|
||||||
|
containers.forEach((container, containerIndex) => {
|
||||||
|
for (const [label, field] of Object.entries(convertedFields)) {
|
||||||
|
const sampleElement = queryElement(container, field.selector);
|
||||||
|
|
||||||
|
if (sampleElement) {
|
||||||
|
const ancestor = findTableAncestor(sampleElement);
|
||||||
|
if (ancestor) {
|
||||||
|
containerFields[containerIndex].tableFields[label] = {
|
||||||
|
...field,
|
||||||
|
tableContext: ancestor.type,
|
||||||
|
cellIndex: ancestor.type === 'TD' ? getCellIndex(ancestor.element) : -1
|
||||||
|
};
|
||||||
|
} else {
|
||||||
|
containerFields[containerIndex].nonTableFields[label] = field;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
containerFields[containerIndex].nonTableFields[label] = field;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const tableData: Array<Record<string, string>> = [];
|
||||||
|
const nonTableData: Array<Record<string, string>> = [];
|
||||||
|
|
||||||
|
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||||
|
const container = containers[containerIndex];
|
||||||
|
const { tableFields } = containerFields[containerIndex];
|
||||||
|
|
||||||
|
if (Object.keys(tableFields).length > 0) {
|
||||||
|
const firstField = Object.values(tableFields)[0];
|
||||||
|
const firstElement = queryElement(container, firstField.selector);
|
||||||
|
let tableContext: Element | null = firstElement;
|
||||||
|
|
||||||
|
while (tableContext && tableContext.tagName !== 'TABLE' && tableContext !== container) {
|
||||||
|
if (tableContext.getRootNode() instanceof ShadowRoot) {
|
||||||
|
tableContext = (tableContext.getRootNode() as ShadowRoot).host;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableContext.tagName === 'IFRAME' || tableContext.tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = tableContext as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
tableContext = frameElement.contentDocument?.body || null;
|
||||||
|
} catch (e) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tableContext = tableContext.parentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tableContext) {
|
||||||
|
const rows: Element[] = [];
|
||||||
|
|
||||||
|
rows.push(...Array.from(tableContext.getElementsByTagName('TR')));
|
||||||
|
|
||||||
|
if (tableContext.tagName === 'IFRAME' || tableContext.tagName === 'FRAME') {
|
||||||
|
try {
|
||||||
|
const frameElement = tableContext as HTMLIFrameElement | HTMLFrameElement;
|
||||||
|
const frameDoc = frameElement.contentDocument || frameElement.contentWindow?.document;
|
||||||
|
if (frameDoc) {
|
||||||
|
rows.push(...Array.from(frameDoc.getElementsByTagName('TR')));
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
console.warn(`Cannot access ${tableContext.tagName.toLowerCase()} rows:`, e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const processedRows = filterRowsBasedOnTag(rows, tableFields);
|
||||||
|
|
||||||
|
for (let rowIndex = 0; rowIndex < Math.min(processedRows.length, limit); rowIndex++) {
|
||||||
|
const record: Record<string, string> = {};
|
||||||
|
const currentRow = processedRows[rowIndex];
|
||||||
|
|
||||||
|
for (const [label, { selector, attribute, cellIndex }] of Object.entries(tableFields)) {
|
||||||
|
let element: Element | null = null;
|
||||||
|
|
||||||
|
if (cellIndex !== undefined && cellIndex >= 0) {
|
||||||
|
let td: Element | null = currentRow.children[cellIndex] || null;
|
||||||
|
|
||||||
|
if (!td && currentRow.shadowRoot) {
|
||||||
|
const shadowCells = currentRow.shadowRoot.children;
|
||||||
|
if (shadowCells && shadowCells.length > cellIndex) {
|
||||||
|
td = shadowCells[cellIndex];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (td) {
|
||||||
|
element = queryElement(td, selector);
|
||||||
|
|
||||||
|
if (!element && selector.split(/(?:>>|:>>)/).pop()?.includes('td:nth-child')) {
|
||||||
|
element = td;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
const tagOnlySelector = selector.split('.')[0];
|
||||||
|
element = queryElement(td, tagOnlySelector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!element) {
|
||||||
|
let currentElement: Element | null = td;
|
||||||
|
while (currentElement && currentElement.children.length > 0) {
|
||||||
|
let foundContentChild = false;
|
||||||
|
for (const child of Array.from(currentElement.children)) {
|
||||||
|
if (extractValue(child, attribute)) {
|
||||||
|
currentElement = child;
|
||||||
|
foundContentChild = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (!foundContentChild) break;
|
||||||
|
}
|
||||||
|
element = currentElement;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
element = queryElement(currentRow, selector);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const value = extractValue(element, attribute);
|
||||||
|
if (value !== null) {
|
||||||
|
record[label] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(record).length > 0) {
|
||||||
|
tableData.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let containerIndex = 0; containerIndex < containers.length; containerIndex++) {
|
||||||
|
if (nonTableData.length >= limit) break;
|
||||||
|
|
||||||
|
const container = containers[containerIndex];
|
||||||
|
const { nonTableFields } = containerFields[containerIndex];
|
||||||
|
|
||||||
|
if (Object.keys(nonTableFields).length > 0) {
|
||||||
|
const record: Record<string, string> = {};
|
||||||
|
|
||||||
|
for (const [label, { selector, attribute }] of Object.entries(nonTableFields)) {
|
||||||
|
const relativeSelector = selector.split(/(?:>>|:>>)/).slice(-1)[0];
|
||||||
|
const element = queryElement(container, relativeSelector);
|
||||||
|
|
||||||
|
if (element) {
|
||||||
|
const value = extractValue(element, attribute);
|
||||||
|
if (value !== null) {
|
||||||
|
record[label] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Object.keys(record).length > 0) {
|
||||||
|
nonTableData.push(record);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const scrapedData = [...tableData, ...nonTableData].slice(0, limit);
|
||||||
|
return scrapedData;
|
||||||
|
},
|
||||||
|
{ listSelector, fields, limit }
|
||||||
|
) as Array<Record<string, string>>;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Registers all event listeners needed for the recording editor session.
|
* Registers all event listeners needed for the recording editor session.
|
||||||
* Should be called only once after the full initialization of the remote browser.
|
* Should be called only once after the full initialization of the remote browser.
|
||||||
@@ -526,6 +1061,26 @@ export class RemoteBrowser {
|
|||||||
this.context = await this.browser.newContext({ viewport: { width, height } });
|
this.context = await this.browser.newContext({ viewport: { width, height } });
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
this.socket.on('extractListData', async (data: {
|
||||||
|
listSelector: string,
|
||||||
|
fields: Record<string, any>,
|
||||||
|
currentListId: number,
|
||||||
|
pagination: any
|
||||||
|
}) => {
|
||||||
|
if (this.currentPage) {
|
||||||
|
const extractedData = await this.extractListData(
|
||||||
|
this.currentPage,
|
||||||
|
data.listSelector,
|
||||||
|
data.fields
|
||||||
|
);
|
||||||
|
|
||||||
|
this.socket.emit('listDataExtracted', {
|
||||||
|
currentListId: data.currentListId,
|
||||||
|
data: extractedData
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
};
|
};
|
||||||
/**
|
/**
|
||||||
* Subscribes the remote browser for a screencast session
|
* Subscribes the remote browser for a screencast session
|
||||||
|
|||||||
@@ -183,6 +183,7 @@ export class WorkflowInterpreter {
|
|||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
...settings,
|
...settings,
|
||||||
|
mode: 'editor',
|
||||||
debugChannel: {
|
debugChannel: {
|
||||||
activeId: (id: any) => {
|
activeId: (id: any) => {
|
||||||
this.activeId = id;
|
this.activeId = id;
|
||||||
|
|||||||
@@ -87,7 +87,7 @@ export const BrowserWindow = () => {
|
|||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { notify } = useGlobalInfoStore();
|
const { notify } = useGlobalInfoStore();
|
||||||
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
|
const { getText, getList, paginationMode, paginationType, limitMode, captureStage } = useActionContext();
|
||||||
const { addTextStep, addListStep } = useBrowserSteps();
|
const { addTextStep, addListStep, updateListStepData } = useBrowserSteps();
|
||||||
|
|
||||||
const { state } = useContext(AuthContext);
|
const { state } = useContext(AuthContext);
|
||||||
const { user } = state;
|
const { user } = state;
|
||||||
@@ -252,6 +252,19 @@ export const BrowserWindow = () => {
|
|||||||
}
|
}
|
||||||
}, [getList, socket, listSelector, paginationMode, paginationType, limitMode]);
|
}, [getList, socket, listSelector, paginationMode, paginationType, limitMode]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (socket) {
|
||||||
|
socket.on('listDataExtracted', (response) => {
|
||||||
|
const { currentListId, data } = response;
|
||||||
|
|
||||||
|
updateListStepData(currentListId, data);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
socket?.off('listDataExtracted');
|
||||||
|
};
|
||||||
|
}, [socket]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
document.addEventListener('mousemove', onMouseMove, false);
|
document.addEventListener('mousemove', onMouseMove, false);
|
||||||
@@ -380,16 +393,27 @@ export const BrowserWindow = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setFields(prevFields => {
|
const updatedFields = {
|
||||||
const updatedFields = {
|
...fields,
|
||||||
...prevFields,
|
[newField.id]: newField
|
||||||
[newField.id]: newField
|
};
|
||||||
};
|
|
||||||
return updatedFields;
|
setFields(updatedFields);
|
||||||
});
|
|
||||||
|
|
||||||
if (listSelector) {
|
if (listSelector) {
|
||||||
addListStep(listSelector, { ...fields, [newField.id]: newField }, currentListId, { type: '', selector: paginationSelector });
|
socket?.emit('extractListData', {
|
||||||
|
listSelector,
|
||||||
|
fields: updatedFields,
|
||||||
|
currentListId,
|
||||||
|
pagination: { type: '', selector: paginationSelector }
|
||||||
|
});
|
||||||
|
|
||||||
|
addListStep(
|
||||||
|
listSelector,
|
||||||
|
updatedFields,
|
||||||
|
currentListId,
|
||||||
|
{ type: '', selector: paginationSelector }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
@@ -441,16 +465,27 @@ export const BrowserWindow = () => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
setFields(prevFields => {
|
const updatedFields = {
|
||||||
const updatedFields = {
|
...fields,
|
||||||
...prevFields,
|
[newField.id]: newField
|
||||||
[newField.id]: newField
|
};
|
||||||
};
|
|
||||||
return updatedFields;
|
setFields(updatedFields);
|
||||||
});
|
|
||||||
|
|
||||||
if (listSelector) {
|
if (listSelector) {
|
||||||
addListStep(listSelector, { ...fields, [newField.id]: newField }, currentListId, { type: '', selector: paginationSelector });
|
socket?.emit('extractListData', {
|
||||||
|
listSelector,
|
||||||
|
fields: updatedFields,
|
||||||
|
currentListId,
|
||||||
|
pagination: { type: '', selector: paginationSelector }
|
||||||
|
});
|
||||||
|
|
||||||
|
addListStep(
|
||||||
|
listSelector,
|
||||||
|
updatedFields,
|
||||||
|
currentListId,
|
||||||
|
{ type: '', selector: paginationSelector }
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ import { WorkflowFile } from "maxun-core";
|
|||||||
import Typography from "@mui/material/Typography";
|
import Typography from "@mui/material/Typography";
|
||||||
import { useGlobalInfoStore } from "../../context/globalInfo";
|
import { useGlobalInfoStore } from "../../context/globalInfo";
|
||||||
import { PaginationType, useActionContext, LimitType } from '../../context/browserActions';
|
import { PaginationType, useActionContext, LimitType } from '../../context/browserActions';
|
||||||
import { useBrowserSteps } from '../../context/browserSteps';
|
import { BrowserStep, useBrowserSteps } from '../../context/browserSteps';
|
||||||
import { useSocketStore } from '../../context/socket';
|
import { useSocketStore } from '../../context/socket';
|
||||||
import { ScreenshotSettings } from '../../shared/types';
|
import { ScreenshotSettings } from '../../shared/types';
|
||||||
import InputAdornment from '@mui/material/InputAdornment';
|
import InputAdornment from '@mui/material/InputAdornment';
|
||||||
@@ -69,7 +69,7 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
|||||||
startAction, finishAction
|
startAction, finishAction
|
||||||
} = useActionContext();
|
} = useActionContext();
|
||||||
|
|
||||||
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField } = useBrowserSteps();
|
const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit } = useBrowserSteps();
|
||||||
const { id, socket } = useSocketStore();
|
const { id, socket } = useSocketStore();
|
||||||
const { t } = useTranslation();
|
const { t } = useTranslation();
|
||||||
|
|
||||||
@@ -349,6 +349,13 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
|||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
const getLatestListStep = (steps: BrowserStep[]) => {
|
||||||
|
const listSteps = steps.filter(step => step.type === 'list');
|
||||||
|
if (listSteps.length === 0) return null;
|
||||||
|
|
||||||
|
return listSteps.sort((a, b) => b.id - a.id)[0];
|
||||||
|
};
|
||||||
|
|
||||||
const handleConfirmListCapture = useCallback(() => {
|
const handleConfirmListCapture = useCallback(() => {
|
||||||
switch (captureStage) {
|
switch (captureStage) {
|
||||||
case 'initial':
|
case 'initial':
|
||||||
@@ -385,6 +392,12 @@ export const RightSidePanel: React.FC<RightSidePanelProps> = ({ onFinishCapture
|
|||||||
notify('error', t('right_panel.errors.invalid_limit'));
|
notify('error', t('right_panel.errors.invalid_limit'));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const latestListStep = getLatestListStep(browserSteps);
|
||||||
|
if (latestListStep) {
|
||||||
|
updateListStepLimit(latestListStep.id, limit);
|
||||||
|
}
|
||||||
|
|
||||||
stopLimitMode();
|
stopLimitMode();
|
||||||
setShowLimitOptions(false);
|
setShowLimitOptions(false);
|
||||||
setIsCaptureListConfirmed(true);
|
setIsCaptureListConfirmed(true);
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import * as React from 'react';
|
import * as React from 'react';
|
||||||
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
import SwipeableDrawer from '@mui/material/SwipeableDrawer';
|
||||||
import Typography from '@mui/material/Typography';
|
import Typography from '@mui/material/Typography';
|
||||||
import { Button, Grid, Tabs, Tab, Box } from '@mui/material';
|
import { Button, Grid, Box } from '@mui/material';
|
||||||
import { useCallback, useEffect, useRef, useState } from "react";
|
import { useCallback, useEffect, useRef, useState } from "react";
|
||||||
import { useSocketStore } from "../../context/socket";
|
import { useSocketStore } from "../../context/socket";
|
||||||
import { Buffer } from 'buffer';
|
import { Buffer } from 'buffer';
|
||||||
@@ -19,6 +19,7 @@ import { SidePanelHeader } from '../recorder/SidePanelHeader';
|
|||||||
import { useGlobalInfoStore } from '../../context/globalInfo';
|
import { useGlobalInfoStore } from '../../context/globalInfo';
|
||||||
import { useThemeMode } from '../../context/theme-provider';
|
import { useThemeMode } from '../../context/theme-provider';
|
||||||
import { useTranslation } from 'react-i18next';
|
import { useTranslation } from 'react-i18next';
|
||||||
|
import { useBrowserSteps } from '../../context/browserSteps';
|
||||||
|
|
||||||
interface InterpretationLogProps {
|
interface InterpretationLogProps {
|
||||||
isOpen: boolean;
|
isOpen: boolean;
|
||||||
@@ -41,6 +42,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
|
|
||||||
const logEndRef = useRef<HTMLDivElement | null>(null);
|
const logEndRef = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const { browserSteps } = useBrowserSteps();
|
||||||
|
|
||||||
|
const [activeActionId, setActiveActionId] = useState<number | null>(null);
|
||||||
|
|
||||||
const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore();
|
const { browserWidth, outputPreviewHeight, outputPreviewWidth } = useBrowserDimensionsStore();
|
||||||
const { socket } = useSocketStore();
|
const { socket } = useSocketStore();
|
||||||
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
|
const { currentWorkflowActionsState, shouldResetInterpretationLog, notify } = useGlobalInfoStore();
|
||||||
@@ -71,33 +76,39 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
scrollLogToBottom();
|
scrollLogToBottom();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const handleSerializableCallback = useCallback(({ type, data }: { type: string, data: any }) => {
|
useEffect(() => {
|
||||||
setLog((prevState) =>
|
if (activeActionId !== null) {
|
||||||
prevState + '\n' + t('interpretation_log.data_sections.serializable_received') + '\n'
|
const textSteps = browserSteps.filter(step => step.type === 'text');
|
||||||
+ JSON.stringify(data, null, 2) + '\n' + t('interpretation_log.data_sections.separator'));
|
if (textSteps.length > 0) {
|
||||||
|
const textDataRow: Record<string, string> = {};
|
||||||
if (type === 'captureList') {
|
|
||||||
setCaptureListData(prev => [...prev, data]);
|
textSteps.forEach(step => {
|
||||||
if (captureListData.length === 0) {
|
textDataRow[step.label] = step.data;
|
||||||
const availableTabs = getAvailableTabs();
|
});
|
||||||
const tabIndex = availableTabs.findIndex(tab => tab.id === 'captureList');
|
|
||||||
if (tabIndex !== -1) setActiveTab(tabIndex);
|
setCaptureTextData([textDataRow]);
|
||||||
}
|
}
|
||||||
} else if (type === 'captureText') {
|
|
||||||
if (Array.isArray(data)) {
|
const listSteps = browserSteps.filter(step => step.type === 'list');
|
||||||
setCaptureTextData(data);
|
if (listSteps.length > 0) {
|
||||||
} else {
|
setCaptureListData(listSteps);
|
||||||
setCaptureTextData([data]);
|
|
||||||
}
|
|
||||||
if (captureTextData.length === 0) {
|
|
||||||
const availableTabs = getAvailableTabs();
|
|
||||||
const tabIndex = availableTabs.findIndex(tab => tab.id === 'captureText');
|
|
||||||
if (tabIndex !== -1) setActiveTab(tabIndex);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
updateActiveTab();
|
||||||
}
|
}
|
||||||
|
}, [activeActionId, browserSteps, t]);
|
||||||
scrollLogToBottom();
|
|
||||||
}, [captureListData.length, captureTextData.length, t]);
|
const updateActiveTab = useCallback(() => {
|
||||||
|
const availableTabs = getAvailableTabs();
|
||||||
|
|
||||||
|
if (captureListData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureList') !== -1) {
|
||||||
|
setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureList'));
|
||||||
|
} else if (captureTextData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureText') !== -1) {
|
||||||
|
setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureText'));
|
||||||
|
} else if (screenshotData.length > 0 && availableTabs.findIndex(tab => tab.id === 'captureScreenshot') !== -1) {
|
||||||
|
setActiveTab(availableTabs.findIndex(tab => tab.id === 'captureScreenshot'));
|
||||||
|
}
|
||||||
|
}, [captureListData.length, captureTextData.length, screenshotData.length]);
|
||||||
|
|
||||||
const handleBinaryCallback = useCallback(({ data, mimetype, type }: { data: any, mimetype: string, type: string }) => {
|
const handleBinaryCallback = useCallback(({ data, mimetype, type }: { data: any, mimetype: string, type: string }) => {
|
||||||
const base64String = Buffer.from(data).toString('base64');
|
const base64String = Buffer.from(data).toString('base64');
|
||||||
@@ -121,6 +132,10 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
scrollLogToBottom();
|
scrollLogToBottom();
|
||||||
}, [screenshotData.length, t]);
|
}, [screenshotData.length, t]);
|
||||||
|
|
||||||
|
const handleActivePairId = useCallback((id: number) => {
|
||||||
|
setActiveActionId(id);
|
||||||
|
}, []);
|
||||||
|
|
||||||
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
const handleCustomValueChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setCustomValue(event.target.value);
|
setCustomValue(event.target.value);
|
||||||
};
|
};
|
||||||
@@ -134,19 +149,21 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
setActiveTab(0);
|
setActiveTab(0);
|
||||||
setCaptureListPage(0);
|
setCaptureListPage(0);
|
||||||
setScreenshotPage(0);
|
setScreenshotPage(0);
|
||||||
|
setActiveActionId(null);
|
||||||
}
|
}
|
||||||
}, [shouldResetInterpretationLog]);
|
}, [shouldResetInterpretationLog]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
socket?.on('log', handleLog);
|
socket?.on('log', handleLog);
|
||||||
socket?.on('serializableCallback', handleSerializableCallback);
|
|
||||||
socket?.on('binaryCallback', handleBinaryCallback);
|
socket?.on('binaryCallback', handleBinaryCallback);
|
||||||
|
socket?.on('activePairId', handleActivePairId);
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
socket?.off('log', handleLog);
|
socket?.off('log', handleLog);
|
||||||
socket?.off('serializableCallback', handleSerializableCallback);
|
|
||||||
socket?.off('binaryCallback', handleBinaryCallback);
|
socket?.off('binaryCallback', handleBinaryCallback);
|
||||||
|
socket?.off('activePairId', handleActivePairId);
|
||||||
};
|
};
|
||||||
}, [socket, handleLog, handleSerializableCallback, handleBinaryCallback]);
|
}, [socket, handleLog, handleBinaryCallback, handleActivePairId]);
|
||||||
|
|
||||||
const getAvailableTabs = useCallback(() => {
|
const getAvailableTabs = useCallback(() => {
|
||||||
const tabs = [];
|
const tabs = [];
|
||||||
@@ -321,45 +338,48 @@ export const InterpretationLog: React.FC<InterpretationLogProps> = ({ isOpen, se
|
|||||||
<Table>
|
<Table>
|
||||||
<TableHead>
|
<TableHead>
|
||||||
<TableRow>
|
<TableRow>
|
||||||
{captureListData[captureListPage] && captureListData[captureListPage].length > 0 &&
|
{Object.values(captureListData[captureListPage]?.fields || {}).map((field: any, index) => (
|
||||||
Object.keys(captureListData[captureListPage][0]).map((column) => (
|
<TableCell
|
||||||
<TableCell
|
key={index}
|
||||||
key={column}
|
sx={{
|
||||||
sx={{
|
borderBottom: '1px solid',
|
||||||
borderBottom: '1px solid',
|
borderColor: darkMode ? '#3a4453' : '#dee2e6',
|
||||||
borderColor: darkMode ? '#3a4453' : '#dee2e6',
|
backgroundColor: darkMode ? '#2a3441' : '#f8f9fa'
|
||||||
backgroundColor: darkMode ? '#2a3441' : '#f8f9fa'
|
}}
|
||||||
}}
|
>
|
||||||
>
|
{field.label}
|
||||||
{column}
|
</TableCell>
|
||||||
</TableCell>
|
))}
|
||||||
))
|
|
||||||
}
|
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHead>
|
</TableHead>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{captureListData[captureListPage] &&
|
{(captureListData[captureListPage]?.data || [])
|
||||||
captureListData[captureListPage].map((row: any, idx: any) => (
|
.slice(0, Math.min(captureListData[captureListPage]?.limit || 10, 5))
|
||||||
<TableRow
|
.map((row: any, rowIndex: any) => (
|
||||||
key={idx}
|
<TableRow
|
||||||
sx={{
|
key={rowIndex}
|
||||||
borderBottom: '1px solid',
|
sx={{
|
||||||
borderColor: darkMode ? '#3a4453' : '#dee2e6'
|
borderBottom: rowIndex < Math.min(
|
||||||
}}
|
(captureListData[captureListPage]?.data?.length || 0),
|
||||||
>
|
Math.min(captureListData[captureListPage]?.limit || 10, 5)
|
||||||
{Object.keys(row).map((column) => (
|
) - 1 ? '1px solid' : 'none',
|
||||||
<TableCell
|
borderColor: darkMode ? '#3a4453' : '#dee2e6'
|
||||||
key={column}
|
}}
|
||||||
sx={{
|
>
|
||||||
borderBottom: 'none',
|
{Object.values(captureListData[captureListPage]?.fields || {}).map((field: any, colIndex) => (
|
||||||
py: 2
|
<TableCell
|
||||||
}}
|
key={colIndex}
|
||||||
>
|
sx={{
|
||||||
{row[column]}
|
borderBottom: 'none',
|
||||||
</TableCell>
|
py: 2
|
||||||
))}
|
}}
|
||||||
</TableRow>
|
>
|
||||||
))}
|
{row[field.label]}
|
||||||
|
</TableCell>
|
||||||
|
))}
|
||||||
|
</TableRow>
|
||||||
|
))
|
||||||
|
}
|
||||||
</TableBody>
|
</TableBody>
|
||||||
</Table>
|
</Table>
|
||||||
</TableContainer>
|
</TableContainer>
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ export interface ListStep {
|
|||||||
limit?: number;
|
limit?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
type BrowserStep = TextStep | ScreenshotStep | ListStep;
|
export type BrowserStep = TextStep | ScreenshotStep | ListStep;
|
||||||
|
|
||||||
export interface SelectorObject {
|
export interface SelectorObject {
|
||||||
selector: string;
|
selector: string;
|
||||||
@@ -44,6 +44,8 @@ interface BrowserStepsContextType {
|
|||||||
deleteBrowserStep: (id: number) => void;
|
deleteBrowserStep: (id: number) => void;
|
||||||
updateBrowserTextStepLabel: (id: number, newLabel: string) => void;
|
updateBrowserTextStepLabel: (id: number, newLabel: string) => void;
|
||||||
updateListTextFieldLabel: (listId: number, fieldKey: string, newLabel: string) => void;
|
updateListTextFieldLabel: (listId: number, fieldKey: string, newLabel: string) => void;
|
||||||
|
updateListStepLimit: (listId: number, limit: number) => void;
|
||||||
|
updateListStepData: (listId: number, extractedData: any[]) => void;
|
||||||
removeListTextField: (listId: number, fieldKey: string) => void;
|
removeListTextField: (listId: number, fieldKey: string) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,6 +144,34 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const updateListStepData = (listId: number, extractedData: any[]) => {
|
||||||
|
setBrowserSteps((prevSteps) => {
|
||||||
|
return prevSteps.map(step => {
|
||||||
|
if (step.type === 'list' && step.id === listId) {
|
||||||
|
return {
|
||||||
|
...step,
|
||||||
|
data: extractedData // Add the extracted data to the step
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateListStepLimit = (listId: number, limit: number) => {
|
||||||
|
setBrowserSteps(prevSteps =>
|
||||||
|
prevSteps.map(step => {
|
||||||
|
if (step.type === 'list' && step.id === listId) {
|
||||||
|
return {
|
||||||
|
...step,
|
||||||
|
limit: limit
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return step;
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
const removeListTextField = (listId: number, fieldKey: string) => {
|
const removeListTextField = (listId: number, fieldKey: string) => {
|
||||||
setBrowserSteps(prevSteps =>
|
setBrowserSteps(prevSteps =>
|
||||||
prevSteps.map(step => {
|
prevSteps.map(step => {
|
||||||
@@ -166,6 +196,8 @@ export const BrowserStepsProvider: React.FC<{ children: React.ReactNode }> = ({
|
|||||||
deleteBrowserStep,
|
deleteBrowserStep,
|
||||||
updateBrowserTextStepLabel,
|
updateBrowserTextStepLabel,
|
||||||
updateListTextFieldLabel,
|
updateListTextFieldLabel,
|
||||||
|
updateListStepLimit,
|
||||||
|
updateListStepData,
|
||||||
removeListTextField,
|
removeListTextField,
|
||||||
}}>
|
}}>
|
||||||
{children}
|
{children}
|
||||||
|
|||||||
Reference in New Issue
Block a user