From 49b0c484b97d3c5a6afc0c195d5edf47dee84491 Mon Sep 17 00:00:00 2001 From: Rohit Date: Thu, 28 Aug 2025 15:29:14 +0530 Subject: [PATCH 01/24] fix: add multiple scroll approach --- maxun-core/src/interpret.ts | 34 +++++++++++++++++++++++++++++----- 1 file changed, 29 insertions(+), 5 deletions(-) diff --git a/maxun-core/src/interpret.ts b/maxun-core/src/interpret.ts index faa473c2..5b695b3d 100644 --- a/maxun-core/src/interpret.ts +++ b/maxun-core/src/interpret.ts @@ -734,10 +734,22 @@ export default class Interpreter extends EventEmitter { return allResults; } - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.evaluate(() => { + const scrollHeight = Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight + ); + + window.scrollTo(0, scrollHeight); + }); await page.waitForTimeout(2000); - const currentHeight = await page.evaluate(() => document.body.scrollHeight); + const currentHeight = await page.evaluate(() => { + return Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight + ); + }); const currentResultCount = allResults.length; if (currentResultCount === previousResultCount) { @@ -1024,10 +1036,22 @@ export default class Interpreter extends EventEmitter { // Wait for content to load and check scroll height await page.waitForTimeout(2000); - await page.evaluate(() => window.scrollTo(0, document.body.scrollHeight)); + await page.evaluate(() => { + const scrollHeight = Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight + ); + + window.scrollTo(0, scrollHeight); + }); await page.waitForTimeout(2000); - - const currentHeight = await page.evaluate(() => document.body.scrollHeight); + + const currentHeight = await page.evaluate(() => { + return Math.max( + document.body.scrollHeight, + document.documentElement.scrollHeight + ); + }); const heightChanged = currentHeight !== previousHeight; previousHeight = currentHeight; From 8ad576967e44b71ca66468e94f92e6f5e77e6b72 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 29 Aug 2025 19:28:01 +0530 Subject: [PATCH 02/24] feat: add runs table ui message --- src/components/run/RunsTable.tsx | 212 +++++++++++++++++-------------- 1 file changed, 119 insertions(+), 93 deletions(-) diff --git a/src/components/run/RunsTable.tsx b/src/components/run/RunsTable.tsx index 2628bdda..c9eb81ec 100644 --- a/src/components/run/RunsTable.tsx +++ b/src/components/run/RunsTable.tsx @@ -378,102 +378,128 @@ export const RunsTable: React.FC = ({ /> - - {Object.entries(groupedRows) - .slice( - accordionPage * accordionsPerPage, - accordionPage * accordionsPerPage + accordionsPerPage - ) - .map(([robotMetaId, data]) => ( - handleAccordionChange(robotMetaId, isExpanded)} - TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering - > - }> - {data[0].name} - - - - - - - {translatedColumns.map((column) => ( - { - if (column.id === 'startedAt' || column.id === 'finishedAt') { - handleSort(column.id, robotMetaId); - } - }} - > - - + + {searchTerm ? t('runstable.placeholder.search') : t('runstable.placeholder.title')} + + + {searchTerm + ? t('recordingtable.search_criteria') + : t('runstable.placeholder.body') + } + + + ) : ( + <> + + {Object.entries(groupedRows) + .slice( + accordionPage * accordionsPerPage, + accordionPage * accordionsPerPage + accordionsPerPage + ) + .map(([robotMetaId, data]) => ( + handleAccordionChange(robotMetaId, isExpanded)} + TransitionProps={{ unmountOnExit: true }} // Optimize accordion rendering + > + }> + {data[0].name} + + +
+ + + + {translatedColumns.map((column) => ( + { + if (column.id === 'startedAt' || column.id === 'finishedAt') { + handleSort(column.id, robotMetaId); } - } - }}> - {column.label} - - {renderSortIcon(column, robotMetaId)} - - - - - ))} - - - - {renderTableRows(data, robotMetaId)} - -
+ }} + > + + + {column.label} + + {renderSortIcon(column, robotMetaId)} + + + + + ))} + + + + {renderTableRows(data, robotMetaId)} + + - handleChangePage(robotMetaId, newPage)} - onRowsPerPageChange={(event) => - handleChangeRowsPerPage(robotMetaId, +event.target.value) - } - rowsPerPageOptions={[10, 25, 50, 100]} - /> -
-
- ))} -
+ handleChangePage(robotMetaId, newPage)} + onRowsPerPageChange={(event) => + handleChangeRowsPerPage(robotMetaId, +event.target.value) + } + rowsPerPageOptions={[10, 25, 50, 100]} + /> + + + ))} + - + + + )} ); }; \ No newline at end of file From bdafb187357a0fdbfe9408c4fc54be4b9aee2811 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 29 Aug 2025 19:30:39 +0530 Subject: [PATCH 03/24] feat: add robots table ui message --- src/components/robot/RecordingsTable.tsx | 97 +++++++++++++++--------- 1 file changed, 62 insertions(+), 35 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index 6ea86f74..eaac87d8 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -513,42 +513,69 @@ export const RecordingsTable = ({ - - - - - {columns.map((column) => ( - - {column.label} - - ))} - - - - {visibleRows.map((row) => ( - - ))} - -
-
- + {filteredRows.length === 0 ? ( + + + {debouncedSearchTerm ? t('recordingtable.placeholder.search') : t('recordingtable.placeholder.title')} + + + {debouncedSearchTerm + ? t('recordingtable.search_criteria') + : t('recordingtable.placeholder.body') + } + + + ) : ( + <> + + + + + {columns.map((column) => ( + + {column.label} + + ))} + + + + {visibleRows.map((row) => ( + + ))} + +
+
+ + + + )} setWarningModalOpen(false)} modalStyle={modalStyle}>
{t('recordingtable.warning_modal.title')} From 561c3e857067950a3b2aa36587a206fe012e27ec Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Fri, 29 Aug 2025 19:37:54 +0530 Subject: [PATCH 04/24] feat: add translations for placeholders --- public/locales/de.json | 11 +++++++++++ public/locales/en.json | 11 +++++++++++ public/locales/es.json | 29 +++++++++++------------------ public/locales/ja.json | 11 +++++++++++ public/locales/tr.json | 11 +++++++++++ public/locales/zh.json | 11 +++++++++++ 6 files changed, 66 insertions(+), 18 deletions(-) diff --git a/public/locales/de.json b/public/locales/de.json index 58d90cff..2e24c82b 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -48,6 +48,12 @@ "options": "Optionen", "heading": "Meine Roboter", "new": "Roboter erstellen", + "search_criteria": "Versuchen Sie, Ihre Suchkriterien anzupassen", + "placeholder": { + "title": "Alles bereit für den Start", + "body": "Roboter, die Sie erstellen, werden hier angezeigt. Klicken Sie auf „Roboter erstellen“, um loszulegen!", + "search": "Keine Roboter entsprechen Ihrer Suche" + }, "modal": { "title": "Geben Sie die URL ein", "login_title": "Ist für diese Website eine Anmeldung erforderlich?", @@ -90,6 +96,11 @@ "settings": "Einstellungen", "search": "Ausführungen suchen...", "sort_tooltip": "Zum Sortieren klicken", + "placeholder": { + "title": "Keine Durchläufe gefunden", + "body": "Hier werden alle Ihre Roboter-Durchläufe angezeigt. Sobald ein Roboter aktiv ist, werden seine Durchläufe hier protokolliert.", + "search": "Keine Durchläufe entsprechen Ihrer Suche" + }, "notifications": { "no_runs": "Keine Ausführungen gefunden. Bitte versuchen Sie es erneut.", "delete_success": "Ausführung erfolgreich gelöscht" diff --git a/public/locales/en.json b/public/locales/en.json index f900089f..ab84b92d 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -48,6 +48,12 @@ "options": "Options", "heading":"My Robots", "new":"Create Robot", + "search_criteria": "Try adjusting your search criteria", + "placeholder": { + "title": "You're All Set to Start", + "body": "Robots you create will appear here. Click \"Create Robot\" to get started!", + "search": "No robots match your search" + }, "modal":{ "title":"Enter the URL", "login_title": "Does this website require logging in?", @@ -90,6 +96,11 @@ "settings":"Settings", "search":"Search Runs...", "sort_tooltip": "Click to sort", + "placeholder": { + "title": "No Runs Found", + "body": "This is where all your robot runs will appear. Once a robot is active, its runs will be logged here.", + "search":"No runs match your search" + }, "notifications": { "no_runs": "No runs found. Please try again.", "delete_success": "Run deleted successfully" diff --git a/public/locales/es.json b/public/locales/es.json index 880f3be9..9072f6e9 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -48,6 +48,12 @@ "options": "Opciones", "heading": "Mis Robots", "new": "Crear Robot", + "search_criteria": "Intente ajustar sus criterios de búsqueda", + "placeholder": { + "title": "Todo listo para empezar", + "body": "Los robots que cree aparecerán aquí. ¡Haga clic en \"Crear robot\" para comenzar!", + "search": "Ningún robot coincide con su búsqueda" + }, "modal": { "title": "Ingresa la URL", "login_title": "¿Este sitio web requiere iniciar sesión?", @@ -90,6 +96,11 @@ "settings": "Ajustes", "search": "Buscar ejecuciones...", "sort_tooltip": "Haga clic para ordenar", + "placeholder": { + "title": "No se encontraron ejecuciones", + "body": "Aquí aparecerán todas las ejecuciones de sus robots. Una vez que un robot esté activo, sus ejecuciones se registrarán aquí.", + "search": "Ninguna ejecución coincide con su búsqueda" + }, "notifications": { "no_runs": "No se encontraron ejecuciones. Por favor, inténtelo de nuevo.", "delete_success": "Ejecución eliminada con éxito" @@ -276,24 +287,6 @@ "reset_successful": "Se reiniciaron correctamente todas las capturas y se volvió al estado inicial" } }, - "interpretation_log": { - "titles": { - "output_preview": "Vista Previa de Datos de Salida", - "screenshot": "Captura de pantalla" - }, - "messages": { - "additional_rows": "Se extraerán filas adicionales de datos una vez que termine la grabación.", - "successful_training": "¡Has entrenado exitosamente al robot para realizar acciones! Haz clic en el botón de abajo para obtener una vista previa de los datos que tu robot extraerá.", - "no_selection": "Parece que aún no has seleccionado nada para extraer. Una vez que lo hagas, el robot mostrará una vista previa de tus selecciones aquí." - }, - "data_sections": { - "binary_received": "---------- Datos binarios de salida recibidos ----------", - "serializable_received": "---------- Datos serializables de salida recibidos ----------", - "mimetype": "tipo MIME: ", - "image_below": "La imagen se muestra a continuación:", - "separator": "--------------------------------------------------" - } - }, "interpretation_buttons": { "buttons": { "preview": "Obtener Vista Previa de Datos de Salida", diff --git a/public/locales/ja.json b/public/locales/ja.json index 05071e8a..68cfd847 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -48,6 +48,12 @@ "options": "オプション", "heading": "私のロボット", "new": "ロボットを作成", + "search_criteria": "検索条件を調整してみてください", + "placeholder": { + "title": "始める準備ができました", + "body": "作成したロボットはここに表示されます。「ロボットを作成」をクリックして始めましょう!", + "search": "検索に一致するロボットはありません" + }, "modal": { "title": "URLを入力してください", "login_title": "このサイトはログインが必要ですか?", @@ -90,6 +96,11 @@ "settings": "設定", "search": "実行を検索...", "sort_tooltip": "クリックして並べ替え", + "placeholder": { + "title": "実行が見つかりません", + "body": "すべてのロボットの実行はここに表示されます。ロボットがアクティブになると、その実行はここに記録されます。", + "search": "検索に一致する実行はありません" + }, "notifications": { "no_runs": "実行が見つかりません。もう一度お試しください。", "delete_success": "実行が正常に削除されました" diff --git a/public/locales/tr.json b/public/locales/tr.json index db2f9ffd..c63704e6 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -48,6 +48,12 @@ "options": "Seçenekler", "heading": "Robotlarım", "new": "Robot Oluştur", + "search_criteria": "Arama kriterlerinizi değiştirmeyi deneyin", + "placeholder": { + "title": "Başlamaya Hazırsınız", + "body": "Oluşturduğunuz robotlar burada görünecektir. Başlamak için \"Robot Oluştur\"a tıklayın!", + "search": "Aramanızla eşleşen robot yok" + }, "modal": { "title": "URL’yi Girin", "login_title": "Bu web sitesine giriş gerekiyor mu?", @@ -90,6 +96,11 @@ "settings": "Ayarlar", "search": "Çalıştırma Ara...", "sort_tooltip": "Sıralamak için tıkla", + "placeholder": { + "title": "Çalıştırma Bulunamadı", + "body": "Tüm robot çalıştırmalarınız burada görünecektir. Bir robot aktif olduğunda, çalıştırmaları buraya kaydedilecektir.", + "search": "Aramanızla eşleşen çalıştırma yok" + }, "notifications": { "no_runs": "Çalıştırma bulunamadı. Lütfen tekrar deneyin.", "delete_success": "Çalıştırma başarıyla silindi" diff --git a/public/locales/zh.json b/public/locales/zh.json index abd5de9f..ff83b560 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -48,6 +48,12 @@ "options": "选项", "heading": "我的机器人", "new": "创建机器人", + "search_criteria": "请尝试调整您的搜索条件", + "placeholder": { + "title": "一切就绪,可以开始了", + "body": "您创建的机器人将显示在这里。点击“创建机器人”即可开始!", + "search": "没有与您搜索匹配的机器人" + }, "modal": { "title": "输入URL", "login_title": "此网站需要登录吗?", @@ -90,6 +96,11 @@ "settings": "设置", "search": "搜索运行记录...", "sort_tooltip": "点击排序", + "placeholder": { + "title": "未找到运行记录", + "body": "您所有的机器人运行记录都将显示在此处。一旦机器人被激活,其运行记录将在这里记下。", + "search": "没有与您搜索匹配的运行记录" + }, "notifications": { "no_runs": "未找到运行记录。请重试。", "delete_success": "运行记录删除成功" From be0548f2449c2ec3b5a8a4b785eb8e80beb724b0 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Sat, 30 Aug 2025 00:31:22 +0530 Subject: [PATCH 05/24] feat: display loader when fetching data --- src/components/robot/RecordingsTable.tsx | 12 ++++++++---- src/components/run/RunsTable.tsx | 10 ++++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index eaac87d8..ed2ba033 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -23,6 +23,7 @@ import { ListItemText, FormControlLabel, Checkbox, + CircularProgress, } from "@mui/material"; import { Schedule, @@ -154,6 +155,7 @@ export const RecordingsTable = ({ const [searchTerm, setSearchTerm] = React.useState(''); const [isWarningModalOpen, setWarningModalOpen] = React.useState(false); const [activeBrowserId, setActiveBrowserId] = React.useState(''); + const [isLoading, setIsLoading] = React.useState(true); const columns = useMemo(() => [ { id: 'interpret', label: t('recordingtable.run'), minWidth: 80 }, @@ -270,6 +272,8 @@ export const RecordingsTable = ({ } catch (error) { console.error('Error fetching recordings:', error); notify('error', t('recordingtable.notifications.fetch_error')); + } finally { + setIsLoading(false); } }, [setRecordings, notify, t]); @@ -405,9 +409,7 @@ export const RecordingsTable = ({ } useEffect(() => { - if (rows.length === 0) { - fetchRecordings(); - } + fetchRecordings(); }, [fetchRecordings]); useEffect(() => { @@ -514,7 +516,9 @@ export const RecordingsTable = ({ - {filteredRows.length === 0 ? ( + {isLoading ? ( + + ) : filteredRows.length === 0 ? ( = ({ const [rows, setRows] = useState([]); const [searchTerm, setSearchTerm] = useState(''); + const [isLoading, setIsLoading] = useState(true); const [paginationStates, setPaginationStates] = useState({}); @@ -224,6 +225,8 @@ export const RunsTable: React.FC = ({ } } catch (error) { notify('error', t('runstable.notifications.fetch_error')); + } finally { + setIsLoading(false); } }, [notify, t]); @@ -231,6 +234,7 @@ export const RunsTable: React.FC = ({ let mounted = true; if (rows.length === 0 || rerenderRuns) { + setIsLoading(true); fetchRuns().then(() => { if (mounted) { setRerenderRuns(false); @@ -378,7 +382,9 @@ export const RunsTable: React.FC = ({ /> - {Object.keys(groupedRows).length === 0 ? ( + {isLoading? ( + + ) : Object.keys(groupedRows).length === 0 ? ( Date: Sun, 31 Aug 2025 12:14:22 +0530 Subject: [PATCH 06/24] feat: centering and sizing loader --- src/components/robot/RecordingsTable.tsx | 12 +++++++++++- src/components/run/RunsTable.tsx | 12 +++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/components/robot/RecordingsTable.tsx b/src/components/robot/RecordingsTable.tsx index ed2ba033..8e294464 100644 --- a/src/components/robot/RecordingsTable.tsx +++ b/src/components/robot/RecordingsTable.tsx @@ -517,7 +517,17 @@ export const RecordingsTable = ({ {isLoading ? ( - + + + ) : filteredRows.length === 0 ? ( = ({ {isLoading? ( - + + + ) : Object.keys(groupedRows).length === 0 ? ( Date: Wed, 3 Sep 2025 00:13:58 +0530 Subject: [PATCH 07/24] feat: calc relative coords input click --- .../recorder/DOMBrowserRenderer.tsx | 20 +++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/components/recorder/DOMBrowserRenderer.tsx b/src/components/recorder/DOMBrowserRenderer.tsx index e409ff64..7e909854 100644 --- a/src/components/recorder/DOMBrowserRenderer.tsx +++ b/src/components/recorder/DOMBrowserRenderer.tsx @@ -572,10 +572,22 @@ export const DOMBrowserRenderer: React.FC = ({ } } - if ( - elementInfo?.tagName !== "INPUT" && - elementInfo?.tagName !== "SELECT" - ) { + if (elementInfo?.tagName === "INPUT" || elementInfo?.tagName === "TEXTAREA") { + const element = target as HTMLElement; + const elementRect = element.getBoundingClientRect(); + const relativeX = iframeX - elementRect.left; + const relativeY = iframeY - elementRect.top; + + socket.emit("dom:click", { + selector, + url: snapshot.baseUrl, + userId: user?.id || "unknown", + elementInfo, + coordinates: { x: relativeX, y: relativeY }, + isSPA: false, + }); + } else if (elementInfo?.tagName !== "SELECT") { + // Handle other elements normally socket.emit("dom:click", { selector, url: snapshot.baseUrl, From bfa941d4ba1b73f7c2b64d4f15e490b9558b5926 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Wed, 3 Sep 2025 00:16:16 +0530 Subject: [PATCH 08/24] feat: perform clicks based on coords --- .../src/browser-management/inputHandlers.ts | 29 ++++++++++++++++++- 1 file changed, 28 insertions(+), 1 deletion(-) diff --git a/server/src/browser-management/inputHandlers.ts b/server/src/browser-management/inputHandlers.ts index 36bfd1c6..c014af3d 100644 --- a/server/src/browser-management/inputHandlers.ts +++ b/server/src/browser-management/inputHandlers.ts @@ -636,13 +636,40 @@ const handleClickAction = async ( const { selector, url, elementInfo, coordinates, isSPA = false } = data; const currentUrl = page.url(); - await page.click(selector); + if (elementInfo && coordinates && (elementInfo.tagName === 'INPUT' || elementInfo.tagName === 'TEXTAREA')) { + try { + const elementHandle = await page.$(selector); + if (elementHandle) { + const boundingBox = await elementHandle.boundingBox(); + if (boundingBox) { + await page.mouse.click( + boundingBox.x + coordinates.x, + boundingBox.y + coordinates.y + ); + } else { + await page.click(selector); + } + } else { + await page.click(selector); + } + } catch (error: any) { + logger.log("warn", `Failed to click at coordinates: ${error.message}`); + await page.click(selector); + } + } else { + await page.click(selector); + } const generator = activeBrowser.generator; await generator.onDOMClickAction(page, data); logger.log("debug", `Click action processed: ${selector}`); + if (elementInfo && (elementInfo.tagName === 'INPUT' || elementInfo.tagName === 'TEXTAREA')) { + logger.log("debug", `Input field click - skipping DOM snapshot for smooth typing`); + return; + } + if (isSPA) { logger.log("debug", `SPA interaction detected for selector: ${selector}`); From eec790f6ebe8910853e06186eca8f061aa3cb561 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Mon, 8 Sep 2025 12:24:04 +0530 Subject: [PATCH 09/24] feat: force container detection --- docker-compose.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.yml b/docker-compose.yml index 67621344..c1b4302e 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -43,6 +43,9 @@ services: # to ensure Playwright works in Docker PLAYWRIGHT_BROWSERS_PATH: /ms-playwright PLAYWRIGHT_SKIP_BROWSER_DOWNLOAD: 0 + # Force container/CI detection for headless mode + CI: "true" + CONTAINER: "true" # DEBUG: pw:api # PWDEBUG: 1 # Enables debugging CHROMIUM_FLAGS: '--disable-gpu --no-sandbox --headless=new' From b58780626024e93bad6330c39d6d48d7351e766a Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Mon, 8 Sep 2025 12:29:34 +0530 Subject: [PATCH 10/24] fix: disable gpu for container stability --- server/src/browser-management/classes/RemoteBrowser.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/server/src/browser-management/classes/RemoteBrowser.ts b/server/src/browser-management/classes/RemoteBrowser.ts index 6d758aef..7e5839c8 100644 --- a/server/src/browser-management/classes/RemoteBrowser.ts +++ b/server/src/browser-management/classes/RemoteBrowser.ts @@ -533,6 +533,7 @@ export class RemoteBrowser { "--disable-extensions", "--no-sandbox", "--disable-dev-shm-usage", + "--disable-gpu", "--force-color-profile=srgb", "--force-device-scale-factor=2", "--ignore-certificate-errors", From be9f6a39ecff3eb619a089d4d9f651ff62f947b8 Mon Sep 17 00:00:00 2001 From: Rohit Rajan Date: Tue, 9 Sep 2025 15:46:57 +0530 Subject: [PATCH 11/24] feat: enhance notifs on confirm captures --- src/components/recorder/RightSidePanel.tsx | 203 ++++++++++++++------- 1 file changed, 139 insertions(+), 64 deletions(-) diff --git a/src/components/recorder/RightSidePanel.tsx b/src/components/recorder/RightSidePanel.tsx index ab580352..d4ffbb70 100644 --- a/src/components/recorder/RightSidePanel.tsx +++ b/src/components/recorder/RightSidePanel.tsx @@ -1,5 +1,5 @@ import React, { useState, useCallback, useEffect, useMemo } from 'react'; -import { Button, Paper, Box, TextField, IconButton } from "@mui/material"; +import { Button, Paper, Box, TextField, IconButton, Tooltip } from "@mui/material"; import EditIcon from '@mui/icons-material/Edit'; import TextFieldsIcon from '@mui/icons-material/TextFields'; import DocumentScannerIcon from '@mui/icons-material/DocumentScanner'; @@ -21,6 +21,7 @@ import ActionDescriptionBox from '../action/ActionDescriptionBox'; import { useThemeMode } from '../../context/theme-provider'; import { useTranslation } from 'react-i18next'; import { useBrowserDimensionsStore } from '../../context/browserDimensions'; +import { emptyWorkflow } from '../../shared/constants'; import { clientListExtractor } from '../../helpers/clientListExtractor'; import { clientSelectorGenerator } from '../../helpers/clientSelectorGenerator'; @@ -54,7 +55,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false); const { panelHeight } = useBrowserDimensionsStore(); - const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, updateDOMMode, currentSnapshot, isDOMMode } = useGlobalInfoStore(); + const { lastAction, notify, currentWorkflowActionsState, setCurrentWorkflowActionsState, resetInterpretationLog, currentListActionId, setCurrentListActionId, currentTextActionId, setCurrentTextActionId, currentScreenshotActionId, setCurrentScreenshotActionId, isDOMMode, setIsDOMMode, currentSnapshot, setCurrentSnapshot, updateDOMMode, initialUrl, setRecordingUrl } = useGlobalInfoStore(); const { getText, startGetText, stopGetText, getList, startGetList, stopGetList, @@ -89,12 +90,6 @@ export const RightSidePanel: React.FC = ({ onFinishCapture } }; - const screenshotModeHandler = (data: any) => { - if (!data.userId || data.userId === id) { - updateDOMMode(false); - } - }; - const domcastHandler = (data: any) => { if (!data.userId || data.userId === id) { if (data.snapshotData && data.snapshotData.snapshot) { @@ -104,12 +99,10 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }; socket.on("dom-mode-enabled", domModeHandler); - socket.on("screenshot-mode-enabled", screenshotModeHandler); socket.on("domcast", domcastHandler); return () => { socket.off("dom-mode-enabled", domModeHandler); - socket.off("screenshot-mode-enabled", screenshotModeHandler); socket.off("domcast", domcastHandler); }; } @@ -243,36 +236,18 @@ export const RightSidePanel: React.FC = ({ onFinishCapture return; } - Object.entries(fields).forEach(([key, field]) => { - if (field.selectorObj?.selector) { - const isFieldXPath = - field.selectorObj.selector.startsWith("//") || - field.selectorObj.selector.startsWith("/"); - console.log( - `Field "${key}" selector:`, - field.selectorObj.selector, - `(XPath: ${isFieldXPath})` - ); - } - }); - const extractedData = clientListExtractor.extractListData( iframeDoc, listSelector, fields, - 5 + 5 ); updateListStepData(currentListId, extractedData); if (extractedData.length === 0) { - console.warn( - "⚠️ No data extracted - this might indicate selector issues" - ); - notify( - "warning", - "No data was extracted. Please verify your selections." - ); + console.warn("⚠️ No data extracted - this might indicate selector issues"); + notify("warning", "No data was extracted. Please verify your selections."); } } catch (error) { console.error("Error in client-side data extraction:", error); @@ -346,12 +321,32 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const handleTextStepConfirm = (id: number) => { const label = textLabels[id]?.trim(); - if (label) { - updateBrowserTextStepLabel(id, label); - setConfirmedTextSteps(prev => ({ ...prev, [id]: true })); - } else { + if (!label) { setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); + return; } + + const existingLabels = browserSteps + .filter(step => + step.type === 'text' && + step.id !== id && + confirmedTextSteps[step.id] && + 'label' in step && + step.label + ) + .map(step => (step as any).label); + + if (existingLabels.includes(label)) { + setErrors(prevErrors => ({ + ...prevErrors, + [id]: t('right_panel.errors.duplicate_label') || `Label "${label}" already exists. Please use a unique label.` + })); + return; + } + + updateBrowserTextStepLabel(id, label); + setConfirmedTextSteps(prev => ({ ...prev, [id]: true })); + setErrors(prevErrors => ({ ...prevErrors, [id]: '' })); }; const handleTextStepDiscard = (id: number) => { @@ -425,14 +420,22 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }; const getTextSettingsObject = useCallback(() => { - const settings: Record = {}; + const settings: Record = {}; + browserSteps.forEach(step => { if (browserStepIdList.includes(step.id)) { return; } if (step.type === 'text' && step.label && step.selectorObj?.selector) { - settings[step.label] = step.selectorObj; + settings[step.label] = { + ...step.selectorObj, + selector: step.selectorObj.selector + }; } setBrowserStepIdList(prevList => [...prevList, step.id]); }); @@ -441,15 +444,24 @@ export const RightSidePanel: React.FC = ({ onFinishCapture }, [browserSteps, browserStepIdList]); const stopCaptureAndEmitGetTextSettings = useCallback(() => { - const hasUnconfirmedTextSteps = browserSteps.some(step => step.type === 'text' && !confirmedTextSteps[step.id]); - if (hasUnconfirmedTextSteps) { + const hasTextStepsForCurrentAction = browserSteps.some(step => step.type === 'text' && step.actionId === currentTextActionId); + if (!hasTextStepsForCurrentAction) { + notify('error', t('right_panel.errors.no_text_captured')); + return; + } + + const hasUnconfirmedTextStepsForCurrentAction = browserSteps.some(step => + step.type === 'text' && + step.actionId === currentTextActionId && + !confirmedTextSteps[step.id] + ); + if (hasUnconfirmedTextStepsForCurrentAction) { notify('error', t('right_panel.errors.confirm_text_fields')); return; } stopGetText(); const settings = getTextSettingsObject(); - const hasTextSteps = browserSteps.some(step => step.type === 'text'); - if (hasTextSteps) { + if (hasTextStepsForCurrentAction) { socket?.emit('action', { action: 'scrapeSchema', settings }); } setIsCaptureTextConfirmed(true); @@ -463,15 +475,29 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const getListSettingsObject = useCallback(() => { let settings: { listSelector?: string; - fields?: Record; - pagination?: { type: string; selector?: string; isShadow?: boolean }; + fields?: Record; + pagination?: { + type: string; + selector?: string; + isShadow?: boolean; + }; limit?: number; - isShadow?: boolean + isShadow?: boolean; } = {}; browserSteps.forEach(step => { if (step.type === 'list' && step.listSelector && Object.keys(step.fields).length > 0) { - const fields: Record = {}; + const fields: Record = {}; Object.entries(step.fields).forEach(([id, field]) => { if (field.selectorObj?.selector) { @@ -487,7 +513,11 @@ export const RightSidePanel: React.FC = ({ onFinishCapture settings = { listSelector: step.listSelector, fields: fields, - pagination: { type: paginationType, selector: step.pagination?.selector, isShadow: step.isShadow }, + pagination: { + type: paginationType, + selector: step.pagination?.selector, + isShadow: step.isShadow + }, limit: parseInt(limitType === 'custom' ? customLimit : limitType), isShadow: step.isShadow }; @@ -503,7 +533,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setShowLimitOptions(false); updateLimitType(''); updateCustomLimit(''); - }, [setShowPaginationOptions, updatePaginationType, setShowLimitOptions, updateLimitType, updateCustomLimit]); + }, [updatePaginationType, updateLimitType, updateCustomLimit]); const handleStopGetList = useCallback(() => { stopGetList(); @@ -512,8 +542,6 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const stopCaptureAndEmitGetListSettings = useCallback(() => { const settings = getListSettingsObject(); - - console.log("rrwebSnapshotHandler", settings); const latestListStep = getLatestListStep(browserSteps); if (latestListStep && settings) { @@ -534,6 +562,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const hasUnconfirmedListTextFields = browserSteps.some(step => step.type === 'list' && + step.actionId === currentListActionId && Object.entries(step.fields).some(([fieldKey]) => !confirmedListTextFields[step.id]?.[fieldKey] ) @@ -549,6 +578,31 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const handleConfirmListCapture = useCallback(() => { switch (captureStage) { case 'initial': + const hasValidListSelectorForCurrentAction = browserSteps.some(step => + step.type === 'list' && + step.actionId === currentListActionId && + step.listSelector && + Object.keys(step.fields).length > 0 + ); + + if (!hasValidListSelectorForCurrentAction) { + notify('error', t('right_panel.errors.capture_list_first')); + return; + } + + const hasUnconfirmedListTextFieldsForCurrentAction = browserSteps.some(step => + step.type === 'list' && + step.actionId === currentListActionId && + Object.entries(step.fields).some(([fieldKey]) => + !confirmedListTextFields[step.id]?.[fieldKey] + ) + ); + + if (hasUnconfirmedListTextFieldsForCurrentAction) { + notify('error', t('right_panel.errors.confirm_all_list_fields')); + return; + } + startPaginationMode(); setShowPaginationOptions(true); setCaptureStage('pagination'); @@ -599,7 +653,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setCaptureStage('initial'); break; } - }, [captureStage, paginationType, limitType, customLimit, startPaginationMode, setShowPaginationOptions, setCaptureStage, getListSettingsObject, notify, stopPaginationMode, startLimitMode, setShowLimitOptions, stopLimitMode, setIsCaptureListConfirmed, stopCaptureAndEmitGetListSettings, t]); + }, [captureStage, paginationType, limitType, customLimit, startPaginationMode, setShowPaginationOptions, setCaptureStage, getListSettingsObject, notify, stopPaginationMode, startLimitMode, setShowLimitOptions, stopLimitMode, setIsCaptureListConfirmed, stopCaptureAndEmitGetListSettings, t, browserSteps, currentListActionId, confirmedListTextFields]); const handleBackCaptureList = useCallback(() => { switch (captureStage) { @@ -616,7 +670,7 @@ export const RightSidePanel: React.FC = ({ onFinishCapture setCaptureStage('initial'); break; } - }, [captureStage, stopLimitMode, setShowLimitOptions, startPaginationMode, setShowPaginationOptions, setCaptureStage, stopPaginationMode]); + }, [captureStage, stopLimitMode, startPaginationMode, stopPaginationMode]); const handlePaginationSettingSelect = (option: PaginationType) => { updatePaginationType(option); @@ -716,14 +770,23 @@ export const RightSidePanel: React.FC = ({ onFinishCapture const isConfirmCaptureDisabled = useMemo(() => { if (captureStage !== 'initial') return false; - const hasValidListSelector = browserSteps.some(step => + const hasValidListSelectorForCurrentAction = browserSteps.some(step => step.type === 'list' && + step.actionId === currentListActionId && step.listSelector && Object.keys(step.fields).length > 0 ); - return !hasValidListSelector || hasUnconfirmedListTextFields; - }, [captureStage, browserSteps, hasUnconfirmedListTextFields]); + const hasUnconfirmedListTextFieldsForCurrentAction = browserSteps.some(step => + step.type === 'list' && + step.actionId === currentListActionId && + Object.entries(step.fields).some(([fieldKey]) => + !confirmedListTextFields[step.id]?.[fieldKey] + ) + ); + + return !hasValidListSelectorForCurrentAction || hasUnconfirmedListTextFieldsForCurrentAction; + }, [captureStage, browserSteps, currentListActionId, confirmedListTextFields]); const theme = useThemeMode(); const isDarkMode = theme.darkMode; @@ -779,21 +842,33 @@ export const RightSidePanel: React.FC = ({ onFinishCapture {t('right_panel.buttons.back')} )} - + + +