diff --git a/public/locales/de.json b/public/locales/de.json index 2e24c82b..9a621016 100644 --- a/public/locales/de.json +++ b/public/locales/de.json @@ -252,7 +252,15 @@ "unable_create_settings": "Listeneinstellungen können nicht erstellt werden. Stellen Sie sicher, dass Sie ein Feld für die Liste definiert haben.", "capture_text_discarded": "Texterfassung verworfen", "capture_list_discarded": "Listenerfassung verworfen", - "label_required": "Beschriftung darf nicht leer sein" + "label_required": "Beschriftung darf nicht leer sein", + "duplicate_label": "Diese Beschriftung existiert bereits. Bitte verwenden Sie eine eindeutige Beschriftung.", + "no_text_captured": "Bitte markieren und wählen Sie Textelemente aus, bevor Sie bestätigen.", + "capture_list_first": "Bitte bewegen Sie die Maus über eine Liste und wählen Sie Textfelder darin aus", + "confirm_all_list_fields": "Bitte bestätigen Sie alle erfassten Listenfelder, bevor Sie fortfahren" + }, + "tooltips": { + "capture_list_first": "Bewegen Sie die Maus über eine Liste und wählen Sie Textfelder darin aus", + "confirm_all_list_fields": "Bitte bestätigen Sie alle erfassten Listenfelder, bevor Sie fortfahren" } }, "save_recording": { @@ -269,7 +277,8 @@ }, "errors": { "user_not_logged": "Benutzer nicht angemeldet. Aufnahme kann nicht gespeichert werden.", - "exists_warning": "Ein Roboter mit diesem Namen existiert bereits, bitte bestätigen Sie das Überschreiben des Roboters." + "exists_warning": "Ein Roboter mit diesem Namen existiert bereits, bitte bestätigen Sie das Überschreiben des Roboters.", + "no_actions_performed": "Roboter kann nicht gespeichert werden. Bitte führen Sie mindestens eine Erfassungsaktion durch, bevor Sie speichern." }, "tooltips": { "saving": "Workflow wird optimiert und gespeichert" diff --git a/public/locales/en.json b/public/locales/en.json index ab84b92d..025e28b4 100644 --- a/public/locales/en.json +++ b/public/locales/en.json @@ -252,7 +252,15 @@ "unable_create_settings": "Unable to create list settings. Make sure you have defined a field for the list.", "capture_text_discarded": "Capture Text Discarded", "capture_list_discarded": "Capture List Discarded", - "label_required": "Label cannot be empty" + "label_required": "Label cannot be empty", + "duplicate_label": "This label already exists. Please use a unique label.", + "no_text_captured": "Please highlight and select text elements before confirming.", + "capture_list_first": "Please hover over a list and select text fields inside it first", + "confirm_all_list_fields": "Please confirm all captured list fields before proceeding" + }, + "tooltips": { + "capture_list_first": "Hover over a list and select text fields inside it first", + "confirm_all_list_fields": "Please confirm all captured list fields before proceeding" } }, "save_recording": { @@ -269,7 +277,8 @@ }, "errors": { "user_not_logged": "User not logged in. Cannot save recording.", - "exists_warning": "Robot with this name already exists, please confirm the Robot's overwrite." + "exists_warning": "Robot with this name already exists, please confirm the Robot's overwrite.", + "no_actions_performed": "Cannot save robot. Please perform at least one capture action before saving." }, "tooltips": { "saving": "Optimizing and saving the workflow" diff --git a/public/locales/es.json b/public/locales/es.json index 9072f6e9..d266565c 100644 --- a/public/locales/es.json +++ b/public/locales/es.json @@ -252,7 +252,15 @@ "unable_create_settings": "No se pueden crear las configuraciones de la lista. Asegúrese de haber definido un campo para la lista.", "capture_text_discarded": "Captura de texto descartada", "capture_list_discarded": "Captura de lista descartada", - "label_required": "La etiqueta no puede estar vacía" + "label_required": "La etiqueta no puede estar vacía", + "duplicate_label": "Esta etiqueta ya existe. Por favor use una etiqueta única.", + "no_text_captured": "Por favor resalte y seleccione elementos de texto antes de confirmar.", + "capture_list_first": "Por favor posicione el cursor sobre una lista y seleccione campos de texto dentro de ella primero", + "confirm_all_list_fields": "Por favor confirme todos los campos de lista capturados antes de continuar" + }, + "tooltips": { + "capture_list_first": "Posicione el cursor sobre una lista y seleccione campos de texto dentro de ella primero", + "confirm_all_list_fields": "Por favor confirme todos los campos de lista capturados antes de continuar" } }, "save_recording": { @@ -269,7 +277,8 @@ }, "errors": { "user_not_logged": "Usuario no conectado. No se puede guardar la grabación.", - "exists_warning": "Ya existe un robot con este nombre, por favor confirme la sobrescritura del robot." + "exists_warning": "Ya existe un robot con este nombre, por favor confirme la sobrescritura del robot.", + "no_actions_performed": "No se puede guardar el robot. Por favor realice al menos una acción de captura antes de guardar." }, "tooltips": { "saving": "Optimizando y guardando el flujo de trabajo" diff --git a/public/locales/ja.json b/public/locales/ja.json index 68cfd847..78613113 100644 --- a/public/locales/ja.json +++ b/public/locales/ja.json @@ -252,7 +252,15 @@ "unable_create_settings": "リスト設定を作成できません。リストのフィールドを定義したことを確認してください。", "capture_text_discarded": "テキスト取得が破棄されました", "capture_list_discarded": "リスト取得が破棄されました", - "label_required": "ラベルは空にできません" + "label_required": "ラベルは空にできません", + "duplicate_label": "このラベルは既に存在します。固有のラベルを使用してください。", + "no_text_captured": "確認する前にテキスト要素をハイライトして選択してください。", + "capture_list_first": "まずリストの上にカーソルを置き、その中のテキストフィールドを選択してください", + "confirm_all_list_fields": "続行する前にすべてのキャプチャされたリストフィールドを確認してください" + }, + "tooltips": { + "capture_list_first": "リストの上にカーソルを置き、その中のテキストフィールドを選択してください", + "confirm_all_list_fields": "すべてのキャプチャされたリストフィールドを確認してください" } }, "save_recording": { @@ -269,7 +277,8 @@ }, "errors": { "user_not_logged": "ユーザーがログインしていません。録画を保存できません。", - "exists_warning": "この名前のロボットは既に存在します。ロボットの上書きを確認してください。" + "exists_warning": "この名前のロボットは既に存在します。ロボットの上書きを確認してください。", + "no_actions_performed": "ロボットを保存できません。保存する前に少なくとも1つのキャプチャアクションを実行してください。" }, "tooltips": { "saving": "ワークフローを最適化して保存中" diff --git a/public/locales/tr.json b/public/locales/tr.json index c63704e6..4fdf6d4a 100644 --- a/public/locales/tr.json +++ b/public/locales/tr.json @@ -252,7 +252,14 @@ "unable_create_settings": "Liste ayarları oluşturulamadı. Bir alan tanımladığınızdan emin olun.", "capture_text_discarded": "Metin Yakalama İptal Edildi", "capture_list_discarded": "Liste Yakalama İptal Edildi", - "label_required": "Etiket boş olamaz" + "label_required": "Etiket boş olamaz", + "no_text_captured": "Henüz metin yakalanmadı. Lütfen önce metin öğeleri seçin.", + "duplicate_label": "Bu etiket zaten mevcut. Lütfen benzersiz bir etiket kullanın.", + "capture_list_first": "Lütfen onaylamadan önce bir liste yakalayın ve alanlar seçin.", + "confirm_all_list_fields": "Lütfen devam etmeden önce tüm liste alanlarını onaylayın." + }, + "tooltips": { + "confirm_all_list_fields": "Lütfen bir sonraki adıma geçmeden önce tüm liste alanlarını onaylayın" } }, "save_recording": { @@ -269,7 +276,8 @@ }, "errors": { "user_not_logged": "Kullanıcı girişi yok. Kaydedilemedi.", - "exists_warning": "Bu isimde robot zaten var; üzerine yazmayı onaylayın." + "exists_warning": "Bu isimde robot zaten var; üzerine yazmayı onaylayın.", + "no_actions_performed": "Robot kaydedilemez. Lütfen kaydetmeden önce en az bir yakalama eylemi gerçekleştirin." }, "tooltips": { "saving": "Akış optimize ediliyor ve kaydediliyor" diff --git a/public/locales/zh.json b/public/locales/zh.json index ff83b560..a9570ea6 100644 --- a/public/locales/zh.json +++ b/public/locales/zh.json @@ -252,7 +252,15 @@ "unable_create_settings": "无法创建列表设置。请确保您已为列表定义了字段。", "capture_text_discarded": "文本捕获已放弃", "capture_list_discarded": "列表捕获已放弃", - "label_required": "标签不能为空" + "label_required": "标签不能为空", + "duplicate_label": "此标签已存在。请使用唯一的标签。", + "no_text_captured": "请在确认之前先高亮并选择文本元素。", + "capture_list_first": "请先将鼠标悬停在列表上并选择其中的文本字段", + "confirm_all_list_fields": "请在继续之前确认所有已捕获的列表字段" + }, + "tooltips": { + "capture_list_first": "将鼠标悬停在列表上并选择其中的文本字段", + "confirm_all_list_fields": "请确认所有已捕获的列表字段" } }, "save_recording": { @@ -269,7 +277,8 @@ }, "errors": { "user_not_logged": "用户未登录。无法保存录制。", - "exists_warning": "已存在同名机器人,请确认是否覆盖机器人。" + "exists_warning": "已存在同名机器人,请确认是否覆盖机器人。", + "no_actions_performed": "无法保存机器人。请在保存之前至少执行一次捕获操作。" }, "tooltips": { "saving": "正在优化并保存工作流程" 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')} )} - + + +