Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
172 changes: 172 additions & 0 deletions legacy/src/RobotDuplicate.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
import React, { useState, useEffect } from 'react';
import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material";
import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { duplicateRecording, getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
import { useTranslation } from 'react-i18next';

interface RobotMeta {
name: string;
id: string;
createdAt: string;
pairs: number;
updatedAt: string;
params: any[];
}

interface RobotWorkflow {
workflow: WhereWhatPair[];
}

interface ScheduleConfig {
runEvery: number;
runEveryUnit: 'MINUTES' | 'HOURS' | 'DAYS' | 'WEEKS' | 'MONTHS';
startFrom: 'SUNDAY' | 'MONDAY' | 'TUESDAY' | 'WEDNESDAY' | 'THURSDAY' | 'FRIDAY' | 'SATURDAY';
atTimeStart?: string;
atTimeEnd?: string;
timezone: string;
lastRunAt?: Date;
nextRunAt?: Date;
cronExpression?: string;
}

export interface RobotSettings {
id: string;
userId?: number;
recording_meta: RobotMeta;
recording: RobotWorkflow;
google_sheet_email?: string | null;
google_sheet_name?: string | null;
google_sheet_id?: string | null;
google_access_token?: string | null;
google_refresh_token?: string | null;
schedule?: ScheduleConfig | null;
}

interface RobotSettingsProps {
isOpen: boolean;
handleStart: (settings: RobotSettings) => void;
handleClose: () => void;
initialSettings?: RobotSettings | null;

}

export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
const { t } = useTranslation();
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
const [robot, setRobot] = useState<RobotSettings | null>(null);
const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore();

useEffect(() => {
if (isOpen) {
getRobot();
}
}, [isOpen]);

useEffect(() => {
if (robot) {
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
setTargetUrl(url);
}
}, [robot]);

const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
setRobot(robot);
} else {
notify('error', t('robot_duplication.notifications.robot_not_found'));
}
}

const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTargetUrl(e.target.value);
};
Comment on lines +56 to +87
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Keep the target URL input controlled

targetUrl flips between '' and undefined, so the <TextField> toggles from controlled to uncontrolled, triggering React warnings. Keep the state as a string and normalize the fetched URL to '' when absent.

-    const [targetUrl, setTargetUrl] = useState<string | undefined>('');
+    const [targetUrl, setTargetUrl] = useState<string>('');
...
-            const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
-            setTargetUrl(url);
+            const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
+            setTargetUrl(url ?? '');
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
export const RobotDuplicationModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
const { t } = useTranslation();
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
const [robot, setRobot] = useState<RobotSettings | null>(null);
const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore();
useEffect(() => {
if (isOpen) {
getRobot();
}
}, [isOpen]);
useEffect(() => {
if (robot) {
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
const url = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
setTargetUrl(url);
}
}, [robot]);
const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
setRobot(robot);
} else {
notify('error', t('robot_duplication.notifications.robot_not_found'));
}
}
const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTargetUrl(e.target.value);
};
export const RobotDuplicationModal = ({
isOpen,
handleStart,
handleClose,
initialSettings,
}: RobotSettingsProps) => {
const { t } = useTranslation();
const [targetUrl, setTargetUrl] = useState<string>('');
const [robot, setRobot] = useState<RobotSettings | null>(null);
const { recordingId, notify, setRerenderRobots } = useGlobalInfoStore();
useEffect(() => {
if (isOpen) {
getRobot();
}
}, [isOpen]);
useEffect(() => {
if (robot) {
const lastPair =
robot.recording.workflow[robot.recording.workflow.length - 1];
const url = lastPair.what.find(a => a.action === 'goto')?.args?.[0];
setTargetUrl(url ?? '');
}
}, [robot]);
const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
setRobot(robot);
} else {
notify('error', t('robot_duplication.notifications.robot_not_found'));
}
};
const handleTargetUrlChange = (e: React.ChangeEvent<HTMLInputElement>) => {
setTargetUrl(e.target.value);
};
// …
}
🤖 Prompt for AI Agents
In legacy/src/RobotDuplicate.tsx around lines 56 to 87, the targetUrl state
toggles between '' and undefined causing controlled/uncontrolled React warnings;
change the useState to consistently be a string (initialize
useState<string>('')), and when deriving the URL from the robot normalize absent
values to an empty string (e.g. setTargetUrl(url || '')) so the TextField
remains controlled; keep the input change handler as-is.


const handleSave = async () => {
if (!robot || !targetUrl) {
notify('error', t('robot_duplication.notifications.url_required'));
return;
}

try {
const success = await duplicateRecording(robot.recording_meta.id, targetUrl);

if (success) {
setRerenderRobots(true);

notify('success', t('robot_duplication.notifications.duplicate_success'));
handleStart(robot);
handleClose();
} else {
notify('error', t('robot_duplication.notifications.duplicate_error'));
}
} catch (error) {
notify('error', t('robot_duplication.notifications.unknown_error'));
console.error('Error updating Target URL:', error);
}
};

return (
<GenericModal
isOpen={isOpen}
onClose={handleClose}
modalStyle={modalStyle}
>
<>
<Typography variant="h5" style={{ marginBottom: '20px' }}>
{t('robot_duplication.title')}
</Typography>
<Box style={{ display: 'flex', flexDirection: 'column' }}>
{
robot && (
<>
<span>
{t('robot_duplication.descriptions.purpose')}
</span>
<br />
<span dangerouslySetInnerHTML={{
__html: t('robot_duplication.descriptions.example', {
url1: '<code>producthunt.com/topics/api</code>',
url2: '<code>producthunt.com/topics/database</code>'
})
}} />
<br />
Comment on lines +128 to +137
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Remove dangerouslySetInnerHTML

Injecting translation strings through dangerouslySetInnerHTML bypasses React’s XSS guardrails (see lint warning). Use <Trans> so placeholders stay safe without manual HTML injection.

-import { useTranslation } from 'react-i18next';
+import { Trans, useTranslation } from 'react-i18next';
...
-                                <span dangerouslySetInnerHTML={{
-                                    __html: t('robot_duplication.descriptions.example', {
-                                        url1: '<code>producthunt.com/topics/api</code>',
-                                        url2: '<code>producthunt.com/topics/database</code>'
-                                    })
-                                }} />
+                                <Trans
+                                    i18nKey="robot_duplication.descriptions.example"
+                                    components={{ code1: <code />, code2: <code /> }}
+                                    values={{
+                                        url1: 'producthunt.com/topics/api',
+                                        url2: 'producthunt.com/topics/database'
+                                    }}
+                                />
🧰 Tools
🪛 ast-grep (0.39.5)

[warning] 130-130: Usage of dangerouslySetInnerHTML detected. This bypasses React's built-in XSS protection. Always sanitize HTML content using libraries like DOMPurify before injecting it into the DOM to prevent XSS attacks.
Context: dangerouslySetInnerHTML
Note: [CWE-79] Improper Neutralization of Input During Web Page Generation [REFERENCES]
- https://reactjs.org/docs/dom-elements.html#dangerouslysetinnerhtml
- https://cwe.mitre.org/data/definitions/79.html

(react-unsafe-html-injection)

🪛 Biome (2.1.2)

[error] 131-131: Avoid passing content using the dangerouslySetInnerHTML prop.

Setting content using code can expose users to cross-site scripting (XSS) attacks

(lint/security/noDangerouslySetInnerHtml)

🤖 Prompt for AI Agents
In legacy/src/RobotDuplicate.tsx around lines 128 to 137, remove the
dangerouslySetInnerHTML usage and replace it with react-i18next's <Trans> so
translation placeholders are rendered safely; update the translation key to use
React nodes (e.g. <0></0> for code segments) and render <Trans
i18nKey="robot_duplication.descriptions.example" components={[<code key="code1"
/>, <code key="code2" />]} values={{ /* keep any values if needed */ }} /> (or
the equivalent components prop), ensuring you import Trans from react-i18next
and pass the two <code> elements as components instead of injecting raw HTML.

<span>
<b>{t('robot_duplication.descriptions.warning')}</b>
</span>
<TextField
label={t('robot_duplication.fields.target_url')}
key={t('robot_duplication.fields.target_url')}
value={targetUrl}
onChange={handleTargetUrlChange}
style={{ marginBottom: '20px', marginTop: '30px' }}
/>
<Box mt={2} display="flex" justifyContent="flex-end">
<Button variant="contained" color="primary" onClick={handleSave}>
{t('robot_duplication.buttons.duplicate')}
</Button>
<Button
onClick={handleClose}
color="primary"
variant="outlined"
style={{ marginLeft: '10px' }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}} >
{t('robot_duplication.buttons.cancel')}
</Button>
</Box>
</>
)
}
</Box>
</>
</GenericModal>
);
};
Loading