import React, { useState, useCallback, useEffect, useMemo } from 'react'; 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'; import { WorkflowFile } from "maxun-core"; import Typography from "@mui/material/Typography"; import { useGlobalInfoStore } from "../../context/globalInfo"; import { PaginationType, useActionContext, LimitType } from '../../context/browserActions'; import { BrowserStep, useBrowserSteps } from '../../context/browserSteps'; import { useSocketStore } from '../../context/socket'; import { ScreenshotSettings } from '../../shared/types'; import InputAdornment from '@mui/material/InputAdornment'; import FormControlLabel from '@mui/material/FormControlLabel'; import FormControl from '@mui/material/FormControl'; import FormLabel from '@mui/material/FormLabel'; import Radio from '@mui/material/Radio'; import RadioGroup from '@mui/material/RadioGroup'; import { getActiveWorkflow } from "../../api/workflow"; 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'; const fetchWorkflow = (id: string, callback: (response: WorkflowFile) => void) => { getActiveWorkflow(id).then( (response) => { if (response) { callback(response); } else { throw new Error("No workflow found"); } } ).catch((error) => { console.log(`Failed to fetch workflow:`,error.message) }) }; interface RightSidePanelProps { onFinishCapture: () => void; } export const RightSidePanel: React.FC = ({ onFinishCapture }) => { const [textLabels, setTextLabels] = useState<{ [id: string]: string }>({}); const [errors, setErrors] = useState<{ [id: string]: string }>({}); const [confirmedTextSteps, setConfirmedTextSteps] = useState<{ [id: string]: boolean }>({}); const [confirmedListTextFields, setConfirmedListTextFields] = useState<{ [listId: string]: { [fieldKey: string]: boolean } }>({}); const [showCaptureList, setShowCaptureList] = useState(true); const [showCaptureScreenshot, setShowCaptureScreenshot] = useState(true); const [showCaptureText, setShowCaptureText] = useState(true); const [hoverStates, setHoverStates] = useState<{ [id: string]: boolean }>({}); const [browserStepIdList, setBrowserStepIdList] = useState([]); const [isCaptureTextConfirmed, setIsCaptureTextConfirmed] = useState(false); const [isCaptureListConfirmed, setIsCaptureListConfirmed] = useState(false); const { panelHeight } = useBrowserDimensionsStore(); 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, getScreenshot, startGetScreenshot, stopGetScreenshot, startPaginationMode, stopPaginationMode, paginationType, updatePaginationType, limitType, customLimit, updateLimitType, updateCustomLimit, stopLimitMode, startLimitMode, captureStage, setCaptureStage, showPaginationOptions, setShowPaginationOptions, showLimitOptions, setShowLimitOptions, workflow, setWorkflow, activeAction, setActiveAction, startAction, finishAction } = useActionContext(); const { browserSteps, updateBrowserTextStepLabel, deleteBrowserStep, addScreenshotStep, updateListTextFieldLabel, removeListTextField, updateListStepLimit, deleteStepsByActionId, updateListStepData, updateScreenshotStepData } = useBrowserSteps(); const { id, socket } = useSocketStore(); const { t } = useTranslation(); const isAnyActionActive = activeAction !== 'none'; const workflowHandler = useCallback((data: WorkflowFile) => { setWorkflow(data); }, [setWorkflow]); useEffect(() => { if (socket) { const domModeHandler = (data: any) => { if (!data.userId || data.userId === id) { updateDOMMode(true); } }; const domcastHandler = (data: any) => { if (!data.userId || data.userId === id) { if (data.snapshotData && data.snapshotData.snapshot) { updateDOMMode(true, data.snapshotData); } } }; socket.on("dom-mode-enabled", domModeHandler); socket.on("domcast", domcastHandler); return () => { socket.off("dom-mode-enabled", domModeHandler); socket.off("domcast", domcastHandler); }; } }, [socket, id, updateDOMMode]); useEffect(() => { if (socket) { socket.on("workflow", workflowHandler); } // fetch the workflow every time the id changes if (id) { fetchWorkflow(id, workflowHandler); } // fetch workflow in 15min intervals let interval = setInterval(() => { if (id) { fetchWorkflow(id, workflowHandler); } }, (1000 * 60 * 15)); return () => { socket?.off("workflow", workflowHandler); clearInterval(interval); }; }, [id, socket, workflowHandler]); useEffect(() => { const hasPairs = workflow.workflow.length > 0; if (!hasPairs) { setShowCaptureList(true); setShowCaptureScreenshot(true); setShowCaptureText(true); return; } const hasScrapeListAction = workflow.workflow.some(pair => pair.what.some(action => action.action === 'scrapeList') ); const hasScreenshotAction = workflow.workflow.some(pair => pair.what.some(action => action.action === 'screenshot') ); const hasScrapeSchemaAction = workflow.workflow.some(pair => pair.what.some(action => action.action === 'scrapeSchema') ); setCurrentWorkflowActionsState({ hasScrapeListAction, hasScreenshotAction, hasScrapeSchemaAction, }); setShowCaptureList(true); setShowCaptureScreenshot(true); setShowCaptureText(true); }, [workflow, setCurrentWorkflowActionsState]); useEffect(() => { if (socket) { socket.on('listDataExtracted', (response) => { if (!isDOMMode) { const { currentListId, data } = response; updateListStepData(currentListId, data); } }); } return () => { socket?.off('listDataExtracted'); }; }, [socket, updateListStepData, isDOMMode]); useEffect(() => { if (socket) { const handleDirectScreenshot = (data: any) => { const screenshotSteps = browserSteps.filter(step => step.type === 'screenshot' && step.actionId === currentScreenshotActionId ); if (screenshotSteps.length > 0) { const latestStep = screenshotSteps[screenshotSteps.length - 1]; updateScreenshotStepData(latestStep.id, data.screenshot); } setCurrentScreenshotActionId(''); }; socket.on('directScreenshotCaptured', handleDirectScreenshot); return () => { socket.off('directScreenshotCaptured', handleDirectScreenshot); }; } }, [socket, id, notify, t, currentScreenshotActionId, updateScreenshotStepData, setCurrentScreenshotActionId]); const extractDataClientSide = useCallback( ( listSelector: string, fields: Record, currentListId: number ) => { if (isDOMMode && currentSnapshot) { try { let iframeElement = document.querySelector( "#dom-browser-iframe" ) as HTMLIFrameElement; if (!iframeElement) { iframeElement = document.querySelector( "#browser-window iframe" ) as HTMLIFrameElement; } if (!iframeElement) { const browserWindow = document.querySelector("#browser-window"); if (browserWindow) { iframeElement = browserWindow.querySelector( "iframe" ) as HTMLIFrameElement; } } if (!iframeElement) { console.error( "Could not find the DOM iframe element for extraction" ); return; } const iframeDoc = iframeElement.contentDocument; if (!iframeDoc) { console.error("Failed to get iframe document"); return; } const extractedData = clientListExtractor.extractListData( iframeDoc, listSelector, fields, 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."); } } catch (error) { console.error("Error in client-side data extraction:", error); notify("error", "Failed to extract data client-side"); } } else { if (!socket) { console.error("Socket not available for backend extraction"); return; } try { socket.emit("extractListData", { listSelector, fields, currentListId, pagination: { type: "", selector: "" }, }); } catch (error) { console.error("Error in backend data extraction:", error); } } }, [isDOMMode, currentSnapshot, updateListStepData, socket, notify] ); const handleMouseEnter = (id: number) => { setHoverStates(prev => ({ ...prev, [id]: true })); }; const handleMouseLeave = (id: number) => { setHoverStates(prev => ({ ...prev, [id]: false })); }; const handleStartGetText = () => { setIsCaptureTextConfirmed(false); const newActionId = `text-${crypto.randomUUID()}`; setCurrentTextActionId(newActionId); startGetText(); } const handleStartGetList = () => { setIsCaptureListConfirmed(false); const newActionId = `list-${crypto.randomUUID()}`; setCurrentListActionId(newActionId); startGetList(); } const handleStartGetScreenshot = () => { const newActionId = `screenshot-${crypto.randomUUID()}`; setCurrentScreenshotActionId(newActionId); startGetScreenshot(); }; const handleTextLabelChange = (id: number, label: string, listId?: number, fieldKey?: string) => { if (listId !== undefined && fieldKey !== undefined) { // Prevent editing if the field is confirmed if (confirmedListTextFields[listId]?.[fieldKey]) { return; } updateListTextFieldLabel(listId, fieldKey, label); } else { setTextLabels(prevLabels => ({ ...prevLabels, [id]: label })); } if (!label.trim()) { setErrors(prevErrors => ({ ...prevErrors, [id]: t('right_panel.errors.label_required') })); } else { setErrors(prevErrors => ({ ...prevErrors, [id]: '' })); } }; const handleTextStepConfirm = (id: number) => { const label = textLabels[id]?.trim(); 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) => { deleteBrowserStep(id); setTextLabels(prevLabels => { const { [id]: _, ...rest } = prevLabels; return rest; }); setErrors(prevErrors => { const { [id]: _, ...rest } = prevErrors; return rest; }); }; const handleTextStepDelete = (id: number) => { deleteBrowserStep(id); setTextLabels(prevLabels => { const { [id]: _, ...rest } = prevLabels; return rest; }); setConfirmedTextSteps(prev => { const { [id]: _, ...rest } = prev; return rest; }); setErrors(prevErrors => { const { [id]: _, ...rest } = prevErrors; return rest; }); }; const handleListTextFieldConfirm = (listId: number, fieldKey: string) => { setConfirmedListTextFields(prev => ({ ...prev, [listId]: { ...(prev[listId] || {}), [fieldKey]: true } })); }; const handleListTextFieldDiscard = (listId: number, fieldKey: string) => { removeListTextField(listId, fieldKey); setConfirmedListTextFields(prev => { const updatedListFields = { ...(prev[listId] || {}) }; delete updatedListFields[fieldKey]; return { ...prev, [listId]: updatedListFields }; }); setErrors(prev => { const { [fieldKey]: _, ...rest } = prev; return rest; }); }; const handleListTextFieldDelete = (listId: number, fieldKey: string) => { removeListTextField(listId, fieldKey); setConfirmedListTextFields(prev => { const updatedListFields = { ...(prev[listId] || {}) }; delete updatedListFields[fieldKey]; return { ...prev, [listId]: updatedListFields }; }); setErrors(prev => { const { [fieldKey]: _, ...rest } = prev; return rest; }); }; const getTextSettingsObject = useCallback(() => { const settings: Record = {}; browserSteps.forEach(step => { if (browserStepIdList.includes(step.id)) { return; } if (step.type === 'text' && step.label && step.selectorObj?.selector) { settings[step.label] = { ...step.selectorObj, selector: step.selectorObj.selector }; } setBrowserStepIdList(prevList => [...prevList, step.id]); }); return settings; }, [browserSteps, browserStepIdList]); const stopCaptureAndEmitGetTextSettings = useCallback(() => { 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(); if (hasTextStepsForCurrentAction) { socket?.emit('action', { action: 'scrapeSchema', settings }); } setIsCaptureTextConfirmed(true); setCurrentTextActionId(''); resetInterpretationLog(); finishAction('text'); onFinishCapture(); clientSelectorGenerator.cleanup(); }, [stopGetText, getTextSettingsObject, socket, browserSteps, confirmedTextSteps, resetInterpretationLog, finishAction, notify, onFinishCapture, t]); const getListSettingsObject = useCallback(() => { let settings: { listSelector?: string; fields?: Record; pagination?: { type: string; selector?: string; isShadow?: boolean; }; limit?: number; isShadow?: boolean; } = {}; browserSteps.forEach(step => { if (step.type === 'list' && step.listSelector && Object.keys(step.fields).length > 0) { const fields: Record = {}; Object.entries(step.fields).forEach(([id, field]) => { if (field.selectorObj?.selector) { fields[field.label] = { selector: field.selectorObj.selector, tag: field.selectorObj.tag, attribute: field.selectorObj.attribute, isShadow: field.selectorObj.isShadow }; } }); settings = { listSelector: step.listSelector, fields: fields, pagination: { type: paginationType, selector: step.pagination?.selector, isShadow: step.isShadow }, limit: parseInt(limitType === 'custom' ? customLimit : limitType), isShadow: step.isShadow }; } }); return settings; }, [browserSteps, paginationType, limitType, customLimit]); const resetListState = useCallback(() => { setShowPaginationOptions(false); updatePaginationType(''); setShowLimitOptions(false); updateLimitType(''); updateCustomLimit(''); }, [updatePaginationType, updateLimitType, updateCustomLimit]); const handleStopGetList = useCallback(() => { stopGetList(); resetListState(); }, [stopGetList, resetListState]); const stopCaptureAndEmitGetListSettings = useCallback(() => { const settings = getListSettingsObject(); const latestListStep = getLatestListStep(browserSteps); if (latestListStep && settings) { extractDataClientSide(latestListStep.listSelector!, latestListStep.fields, latestListStep.id); socket?.emit('action', { action: 'scrapeList', settings }); } else { notify('error', t('right_panel.errors.unable_create_settings')); } handleStopGetList(); setCurrentListActionId(''); resetInterpretationLog(); finishAction('list'); onFinishCapture(); clientSelectorGenerator.cleanup(); }, [getListSettingsObject, socket, notify, handleStopGetList, resetInterpretationLog, finishAction, onFinishCapture, t, browserSteps, extractDataClientSide]); const hasUnconfirmedListTextFields = browserSteps.some(step => step.type === 'list' && step.actionId === currentListActionId && Object.entries(step.fields).some(([fieldKey]) => !confirmedListTextFields[step.id]?.[fieldKey] ) ); 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(() => { 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'); break; case 'pagination': if (!paginationType) { notify('error', t('right_panel.errors.select_pagination')); return; } const settings = getListSettingsObject(); const paginationSelector = settings.pagination?.selector; if (['clickNext', 'clickLoadMore'].includes(paginationType) && !paginationSelector) { notify('error', t('right_panel.errors.select_pagination_element')); return; } stopPaginationMode(); setShowPaginationOptions(false); startLimitMode(); setShowLimitOptions(true); setCaptureStage('limit'); break; case 'limit': if (!limitType || (limitType === 'custom' && !customLimit)) { notify('error', t('right_panel.errors.select_limit')); return; } const limit = limitType === 'custom' ? parseInt(customLimit) : parseInt(limitType); if (isNaN(limit) || limit <= 0) { notify('error', t('right_panel.errors.invalid_limit')); return; } const latestListStep = getLatestListStep(browserSteps); if (latestListStep) { updateListStepLimit(latestListStep.id, limit); } stopLimitMode(); setShowLimitOptions(false); setIsCaptureListConfirmed(true); stopCaptureAndEmitGetListSettings(); setCaptureStage('complete'); break; case 'complete': setCaptureStage('initial'); break; } }, [captureStage, paginationType, limitType, customLimit, startPaginationMode, setShowPaginationOptions, setCaptureStage, getListSettingsObject, notify, stopPaginationMode, startLimitMode, setShowLimitOptions, stopLimitMode, setIsCaptureListConfirmed, stopCaptureAndEmitGetListSettings, t, browserSteps, currentListActionId, confirmedListTextFields]); const handleBackCaptureList = useCallback(() => { switch (captureStage) { case 'limit': stopLimitMode(); setShowLimitOptions(false); startPaginationMode(); setShowPaginationOptions(true); setCaptureStage('pagination'); break; case 'pagination': stopPaginationMode(); setShowPaginationOptions(false); setCaptureStage('initial'); break; } }, [captureStage, stopLimitMode, startPaginationMode, stopPaginationMode]); const handlePaginationSettingSelect = (option: PaginationType) => { updatePaginationType(option); }; const discardGetText = useCallback(() => { stopGetText(); if (currentTextActionId) { const stepsToDelete = browserSteps .filter(step => step.type === 'text' && step.actionId === currentTextActionId) .map(step => step.id); deleteStepsByActionId(currentTextActionId); setTextLabels(prevLabels => { const newLabels = { ...prevLabels }; stepsToDelete.forEach(id => { delete newLabels[id]; }); return newLabels; }); setErrors(prevErrors => { const newErrors = { ...prevErrors }; stepsToDelete.forEach(id => { delete newErrors[id]; }); return newErrors; }); setConfirmedTextSteps(prev => { const newConfirmed = { ...prev }; stepsToDelete.forEach(id => { delete newConfirmed[id]; }); return newConfirmed; }); } setCurrentTextActionId(''); setIsCaptureTextConfirmed(false); clientSelectorGenerator.cleanup(); notify('error', t('right_panel.errors.capture_text_discarded')); }, [currentTextActionId, browserSteps, stopGetText, deleteStepsByActionId, notify, t]); const discardGetList = useCallback(() => { stopGetList(); if (currentListActionId) { const listStepsToDelete = browserSteps .filter(step => step.type === 'list' && step.actionId === currentListActionId) .map(step => step.id); deleteStepsByActionId(currentListActionId); setConfirmedListTextFields(prev => { const newConfirmed = { ...prev }; listStepsToDelete.forEach(id => { delete newConfirmed[id]; }); return newConfirmed; }); } resetListState(); stopPaginationMode(); stopLimitMode(); setShowPaginationOptions(false); setShowLimitOptions(false); setCaptureStage('initial'); setCurrentListActionId(''); setIsCaptureListConfirmed(false); clientSelectorGenerator.cleanup(); notify('error', t('right_panel.errors.capture_list_discarded')); }, [currentListActionId, browserSteps, stopGetList, deleteStepsByActionId, resetListState, setShowPaginationOptions, setShowLimitOptions, setCaptureStage, notify, t]); const captureScreenshot = (fullPage: boolean) => { const screenshotSettings = { fullPage, type: 'png' as const, timeout: 30000, animations: 'allow' as const, caret: 'hide' as const, scale: 'device' as const, }; socket?.emit('captureDirectScreenshot', screenshotSettings); socket?.emit('action', { action: 'screenshot', settings: screenshotSettings }); addScreenshotStep(fullPage, currentScreenshotActionId); stopGetScreenshot(); resetInterpretationLog(); finishAction('screenshot'); clientSelectorGenerator.cleanup(); onFinishCapture(); }; const isConfirmCaptureDisabled = useMemo(() => { if (captureStage !== 'initial') return false; const hasValidListSelectorForCurrentAction = browserSteps.some(step => step.type === 'list' && step.actionId === currentListActionId && step.listSelector && Object.keys(step.fields).length > 0 ); 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; return ( {!isAnyActionActive && ( <> {showCaptureList && ( )} {showCaptureText && ( )} {showCaptureScreenshot && ( )} )} {getList && ( {(captureStage === 'pagination' || captureStage === 'limit') && ( )} {showPaginationOptions && ( {t('right_panel.pagination.title')} )} {showLimitOptions && ( {t('right_panel.limit.title')} updateLimitType(e.target.value as LimitType)} sx={{ display: 'flex', flexDirection: 'column', width: '100%', }} > } label="10" /> } label="100" />
} label={t('right_panel.limit.custom')} /> {limitType === 'custom' && ( ) => { const value = parseInt(e.target.value); if (e.target.value === '' || value >= 1) { updateCustomLimit(e.target.value); } }} inputProps={{ min: 1, onKeyPress: (e: React.KeyboardEvent) => { const value = (e.target as HTMLInputElement).value + e.key; if (parseInt(value) < 1) { e.preventDefault(); } } }} placeholder={t('right_panel.limit.enter_number')} sx={{ marginLeft: '10px', '& input': { padding: '10px', }, width: '150px', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black', }} /> )}
)}
)} {getText && ( )} {getScreenshot && ( )}
{browserSteps.map(step => ( handleMouseEnter(step.id)} onMouseLeave={() => handleMouseLeave(step.id)} sx={{ padding: '10px', margin: '11px', borderRadius: '5px', position: 'relative', background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }}> { step.type === 'text' && ( <> handleTextLabelChange(step.id, e.target.value)} fullWidth size="small" margin="normal" error={!!errors[step.id]} helperText={errors[step.id]} InputProps={{ readOnly: confirmedTextSteps[step.id], startAdornment: ( ) }} sx={{ background: isDarkMode ? "#1E2124" : 'white', color: isDarkMode ? "white" : 'black' }} /> ) }} /> {!confirmedTextSteps[step.id] ? ( ) : !isCaptureTextConfirmed && ( )} )} {step.type === 'screenshot' && ( {step.fullPage ? t('right_panel.screenshot.display_fullpage') : t('right_panel.screenshot.display_visible')} )} {step.type === 'list' && ( Object.entries(step.fields).length === 0 ? ( {t('right_panel.messages.list_empty')} ) : ( <> {t('right_panel.messages.list_selected')} {Object.entries(step.fields).map(([key, field]) => ( handleTextLabelChange(field.id, e.target.value, step.id, key)} fullWidth margin="normal" InputProps={{ readOnly: confirmedListTextFields[field.id]?.[key], startAdornment: ( ) }} /> ) }} /> {!confirmedListTextFields[step.id]?.[key] && ( )} ))} ) )} ))}
); };