Skip to content

Conversation

amhsirak
Copy link
Member

@amhsirak amhsirak commented Oct 6, 2025

moves all robot modals to legacy since we moved to pages.

@amhsirak amhsirak requested a review from RohitR311 October 6, 2025 12:37
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 95a4d3c and 8cb7c52.

📒 Files selected for processing (3)
  • legacy/src/RobotDuplicate.tsx (1 hunks)
  • legacy/src/RobotEdit.tsx (1 hunks)
  • legacy/src/RobotSettings.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
legacy/src/RobotSettings.tsx (3)
src/context/globalInfo.tsx (1)
  • useGlobalInfoStore (183-183)
src/api/storage.ts (1)
  • getStoredRecording (80-92)
src/components/ui/GenericModal.tsx (1)
  • GenericModal (13-29)
legacy/src/RobotEdit.tsx (3)
src/context/globalInfo.tsx (1)
  • useGlobalInfoStore (183-183)
src/api/storage.ts (2)
  • getStoredRecording (80-92)
  • updateRecording (31-48)
src/components/ui/GenericModal.tsx (1)
  • GenericModal (13-29)
legacy/src/RobotDuplicate.tsx (3)
src/context/globalInfo.tsx (1)
  • useGlobalInfoStore (183-183)
src/api/storage.ts (2)
  • getStoredRecording (80-92)
  • duplicateRecording (50-64)
src/components/ui/GenericModal.tsx (1)
  • GenericModal (13-29)
🪛 ast-grep (0.39.5)
legacy/src/RobotDuplicate.tsx

[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)
legacy/src/RobotDuplicate.tsx

[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)

Comment on lines +56 to +87
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);
};
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.

Comment on lines +128 to +137
{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 />
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.

Comment on lines +323 to +369
const handleLimitChange = (pairIndex: number, actionIndex: number, argIndex: number, newLimit: number) => {
setRobot((prev) => {
if (!prev) return prev;

const updatedWorkflow = [...prev.recording.workflow];
if (
updatedWorkflow.length > pairIndex &&
updatedWorkflow[pairIndex]?.what &&
updatedWorkflow[pairIndex].what.length > actionIndex &&
updatedWorkflow[pairIndex].what[actionIndex].args &&
updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex
) {
updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = newLimit;

setScrapeListLimits(prev => {
return prev.map(item => {
if (item.pairIndex === pairIndex &&
item.actionIndex === actionIndex &&
item.argIndex === argIndex) {
return { ...item, currentLimit: newLimit };
}
return item;
});
});
}

return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
});
};

const handleTargetUrlChange = (newUrl: string) => {
setRobot((prev) => {
if (!prev) return prev;

const updatedWorkflow = [...prev.recording.workflow];
const lastPairIndex = updatedWorkflow.length - 1;

if (lastPairIndex >= 0) {
const gotoAction = updatedWorkflow[lastPairIndex]?.what?.find(action => action.action === "goto");
if (gotoAction && gotoAction.args && gotoAction.args.length > 0) {
gotoAction.args[0] = newUrl;
}
}

return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
});
};
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

Avoid mutating the existing workflow state

Both handleLimitChange and handleTargetUrlChange clone only the outer workflow array, then mutate nested objects in place. That mutates prev state directly, breaking React’s immutability expectations and leading to subtle bugs. Rebuild only the touched branch immutably before writing the new limit / URL.

-            const updatedWorkflow = [...prev.recording.workflow];
-            if (
-                updatedWorkflow.length > pairIndex &&
-                updatedWorkflow[pairIndex]?.what &&
-                updatedWorkflow[pairIndex].what.length > actionIndex &&
-                updatedWorkflow[pairIndex].what[actionIndex].args &&
-                updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex
-            ) {
-                updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = newLimit;
-                
-                setScrapeListLimits(prev => {
+            const updatedWorkflow = prev.recording.workflow.map((pair, idx) => {
+                if (idx !== pairIndex || !pair.what) return pair;
+
+                return {
+                    ...pair,
+                    what: pair.what.map((action, aIdx) => {
+                        if (aIdx !== actionIndex || !action.args || action.args.length <= argIndex) {
+                            return action;
+                        }
+
+                        const args = [...action.args];
+                        const arg = args[argIndex];
+                        if (typeof arg !== 'object' || arg === null) {
+                            return action;
+                        }
+
+                        args[argIndex] = { ...arg, limit: newLimit };
+                        return { ...action, args };
+                    })
+                };
+            });
+
+            setScrapeListLimits(prev => {
                 return prev.map(item => {
                     if (item.pairIndex === pairIndex && 
                         item.actionIndex === actionIndex && 
                         item.argIndex === argIndex) {
                         return { ...item, currentLimit: newLimit };
                     }
                     return item;
                 });
-                });
-            }
+            });
...
-            const updatedWorkflow = [...prev.recording.workflow];
-            const lastPairIndex = updatedWorkflow.length - 1;
-
-            if (lastPairIndex >= 0) {
-                const gotoAction = updatedWorkflow[lastPairIndex]?.what?.find(action => action.action === "goto");
-                if (gotoAction && gotoAction.args && gotoAction.args.length > 0) {
-                    gotoAction.args[0] = newUrl;
-                }
-            }
+            const updatedWorkflow = prev.recording.workflow.map((pair, idx) => {
+                if (idx !== prev.recording.workflow.length - 1 || !pair.what) {
+                    return pair;
+                }
+
+                return {
+                    ...pair,
+                    what: pair.what.map(action => {
+                        if (action.action !== "goto" || !action.args || action.args.length === 0) {
+                            return action;
+                        }
+
+                        const args = [...action.args];
+                        args[0] = newUrl;
+                        return { ...action, args };
+                    })
+                };
+            });
📝 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
const handleLimitChange = (pairIndex: number, actionIndex: number, argIndex: number, newLimit: number) => {
setRobot((prev) => {
if (!prev) return prev;
const updatedWorkflow = [...prev.recording.workflow];
if (
updatedWorkflow.length > pairIndex &&
updatedWorkflow[pairIndex]?.what &&
updatedWorkflow[pairIndex].what.length > actionIndex &&
updatedWorkflow[pairIndex].what[actionIndex].args &&
updatedWorkflow[pairIndex].what[actionIndex].args.length > argIndex
) {
updatedWorkflow[pairIndex].what[actionIndex].args[argIndex].limit = newLimit;
setScrapeListLimits(prev => {
return prev.map(item => {
if (item.pairIndex === pairIndex &&
item.actionIndex === actionIndex &&
item.argIndex === argIndex) {
return { ...item, currentLimit: newLimit };
}
return item;
});
});
}
return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
});
};
const handleTargetUrlChange = (newUrl: string) => {
setRobot((prev) => {
if (!prev) return prev;
const updatedWorkflow = [...prev.recording.workflow];
const lastPairIndex = updatedWorkflow.length - 1;
if (lastPairIndex >= 0) {
const gotoAction = updatedWorkflow[lastPairIndex]?.what?.find(action => action.action === "goto");
if (gotoAction && gotoAction.args && gotoAction.args.length > 0) {
gotoAction.args[0] = newUrl;
}
}
return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
});
};
const handleLimitChange = (
pairIndex: number,
actionIndex: number,
argIndex: number,
newLimit: number
) => {
setRobot((prev) => {
if (!prev) return prev;
const updatedWorkflow = prev.recording.workflow.map((pair, idx) => {
if (idx !== pairIndex || !pair.what) return pair;
return {
...pair,
what: pair.what.map((action, aIdx) => {
if (aIdx !== actionIndex || !action.args || action.args.length <= argIndex) {
return action;
}
const args = [...action.args];
const arg = args[argIndex];
// Only update if it's an object with a limit property
if (typeof arg !== 'object' || arg === null) {
return action;
}
args[argIndex] = { ...arg, limit: newLimit };
return { ...action, args };
}),
};
});
setScrapeListLimits((prev) =>
prev.map((item) =>
item.pairIndex === pairIndex &&
item.actionIndex === actionIndex &&
item.argIndex === argIndex
? { ...item, currentLimit: newLimit }
: item
)
);
return {
...prev,
recording: { ...prev.recording, workflow: updatedWorkflow },
};
});
};
const handleTargetUrlChange = (newUrl: string) => {
setRobot((prev) => {
if (!prev) return prev;
const updatedWorkflow = prev.recording.workflow.map((pair, idx) => {
// only change the last pair in the workflow
if (idx !== prev.recording.workflow.length - 1 || !pair.what) {
return pair;
}
return {
...pair,
what: pair.what.map((action) => {
if (action.action !== "goto" || !action.args || action.args.length === 0) {
return action;
}
const args = [...action.args];
args[0] = newUrl;
return { ...action, args };
}),
};
});
return {
...prev,
recording: { ...prev.recording, workflow: updatedWorkflow },
};
});
};

Comment on lines +401 to +431
const renderCredentialFields = (selectors: string[], headerText: string, defaultType: 'text' | 'password' = 'text') => {
if (selectors.length === 0) return null;

return (
<>
{selectors.map((selector, index) => {
const isVisible = showPasswords[selector];

return (
<TextField
key={selector}
type={isVisible ? 'text' : 'password'}
label={headerText === 'Other' ? `${`Input`} ${index + 1}` : headerText}
value={credentials[selector]?.value || ''}
onChange={(e) => handleCredentialChange(selector, e.target.value)}
style={{ marginBottom: '20px' }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Show input"
onClick={() => handleClickShowPassword(selector)}
edge="end"
disabled={!credentials[selector]?.value}
>
{isVisible ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
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

Unintentionally masking non-password credential inputs

Every credential field defaults to type="password" unless toggled because the code ignores defaultType. As a result, usernames/emails render hidden, which is a functional regression. Respect the intended default type and only attach the show/hide toggle to password fields.

-                    const isVisible = showPasswords[selector];
+                    const isVisible = showPasswords[selector] ?? false;
+                    const isPasswordField = defaultType === 'password';
+                    const inputType = isPasswordField
+                        ? (isVisible ? 'text' : 'password')
+                        : 'text';
...
-                        <TextField
+                        <TextField
                             key={selector}
-                            type={isVisible ? 'text' : 'password'}
+                            type={inputType}
...
-                            InputProps={{
-                                endAdornment: (
-                                    <InputAdornment position="end">
-                                        <IconButton
-                                            aria-label="Show input"
-                                            onClick={() => handleClickShowPassword(selector)}
-                                            edge="end"
-                                            disabled={!credentials[selector]?.value}
-                                        >
-                                            {isVisible ? <Visibility /> : <VisibilityOff />}
-                                        </IconButton>
-                                    </InputAdornment>
-                                ),
-                            }}
+                            InputProps={isPasswordField ? {
+                                endAdornment: (
+                                    <InputAdornment position="end">
+                                        <IconButton
+                                            aria-label={t('robot_edit.toggle_password_visibility')}
+                                            onClick={() => handleClickShowPassword(selector)}
+                                            edge="end"
+                                            disabled={!credentials[selector]?.value}
+                                        >
+                                            {isVisible ? <Visibility /> : <VisibilityOff />}
+                                        </IconButton>
+                                    </InputAdornment>
+                                ),
+                            } : undefined}
📝 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
const renderCredentialFields = (selectors: string[], headerText: string, defaultType: 'text' | 'password' = 'text') => {
if (selectors.length === 0) return null;
return (
<>
{selectors.map((selector, index) => {
const isVisible = showPasswords[selector];
return (
<TextField
key={selector}
type={isVisible ? 'text' : 'password'}
label={headerText === 'Other' ? `${`Input`} ${index + 1}` : headerText}
value={credentials[selector]?.value || ''}
onChange={(e) => handleCredentialChange(selector, e.target.value)}
style={{ marginBottom: '20px' }}
InputProps={{
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label="Show input"
onClick={() => handleClickShowPassword(selector)}
edge="end"
disabled={!credentials[selector]?.value}
>
{isVisible ? <Visibility /> : <VisibilityOff />}
</IconButton>
</InputAdornment>
),
}}
/>
const renderCredentialFields = (
selectors: string[],
headerText: string,
defaultType: 'text' | 'password' = 'text'
) => {
if (selectors.length === 0) return null;
return (
<>
{selectors.map((selector, index) => {
const isVisible = showPasswords[selector] ?? false;
const isPasswordField = defaultType === 'password';
const inputType = isPasswordField
? (isVisible ? 'text' : 'password')
: 'text';
return (
<TextField
key={selector}
type={inputType}
label={
headerText === 'Other'
? `${`Input`} ${index + 1}`
: headerText
}
value={credentials[selector]?.value || ''}
onChange={(e) =>
handleCredentialChange(selector, e.target.value)
}
style={{ marginBottom: '20px' }}
- InputProps={{
- endAdornment: (
- <InputAdornment position="end">
- <IconButton
- aria-label="Show input"
- onClick={() => handleClickShowPassword(selector)}
- edge="end"
- disabled={!credentials[selector]?.value}
- >
- {isVisible ? <Visibility /> : <VisibilityOff />}
- </IconButton>
- </InputAdornment>
- ),
InputProps={
isPasswordField
? {
endAdornment: (
<InputAdornment position="end">
<IconButton
aria-label={t(
'robot_edit.toggle_password_visibility'
)}
onClick={() =>
handleClickShowPassword(selector)
}
edge="end"
disabled={
!credentials[selector]?.value
}
>
{isVisible ? (
<Visibility />
) : (
<VisibilityOff />
)}
</IconButton>
</InputAdornment>
),
}
: undefined
}
/>
);
})}
</>
);
};
🤖 Prompt for AI Agents
In legacy/src/RobotEdit.tsx around lines 401 to 431, the input type is always
treated as a password because the code ignores the defaultType param and always
toggles visibility; change it so the field uses defaultType by default and only
applies the show/hide behavior when defaultType === 'password'. Concretely,
compute the input type as: if defaultType === 'password' then use isVisible ?
'text' : 'password' else use defaultType, and only render the
InputAdornment/IconButton toggle when defaultType === 'password' (keeping the
disabled logic when the credential value is empty).

@getmaxun getmaxun deleted a comment from coderabbitai bot Oct 6, 2025
Copy link

coderabbitai bot commented Oct 6, 2025

Caution

Failed to replace (edit) comment. This is likely due to insufficient permissions or the comment being deleted.

Error details
{"name":"HttpError","status":404,"request":{"method":"PATCH","url":"https://api.github.com/repos/getmaxun/maxun/issues/comments/3371428944","headers":{"accept":"application/vnd.github.v3+json","user-agent":"octokit.js/0.0.0-development octokit-core.js/7.0.5 Node.js/24","authorization":"token [REDACTED]","content-type":"application/json; charset=utf-8"},"body":{"body":"<!-- This is an auto-generated comment: summarize by coderabbit.ai -->\n<!-- walkthrough_start -->\n\n## Walkthrough\nAdds four new React modal components to legacy/src: RobotDuplicationModal, RobotEditModal, RobotSettingsModal, and ScheduleSettingsModal. Each modal loads a recording by ID when opened, derives/exposes workflow-derived fields (e.g., last \"goto\" target URL), and performs duplication, update, read, or schedule actions with notifications and rerender triggers.\n\n## Changes\n| Cohort / File(s) | Summary of Changes |\n|---|---|\n| **Duplication modal**<br>`legacy/src/RobotDuplicate.tsx` | New `RobotDuplicationModal` component: fetches recording by `recordingId`, derives last `goto` targetUrl, allows editing targetUrl, calls `duplicateRecording`, handles success/error notifications and triggers `setRerenderRobots`. Exports `RobotSettings` type. |\n| **Edit modal**<br>`legacy/src/RobotEdit.tsx` | New `RobotEditModal` component: loads recording, parses and groups credentials, detects list limits, allows editing name/URL/credentials/limits, constructs update payload and calls `updateRecording`, handles notifications and rerender. Exports `RobotSettings` type. |\n| **Read-only settings modal**<br>`legacy/src/RobotSettings.tsx` | New `RobotSettingsModal` component: loads recording, derives targetUrl and meta fields, fetches creator email if present, displays read-only robot details inside `GenericModal`. Exports `RobotSettings` and `modalStyle`. |\n| **Schedule modal**<br>`legacy/src/ScheduleSettings.tsx` | New `ScheduleSettingsModal` component and `ScheduleSettings` interface: fetches/saves/deletes schedule via schedule APIs, renders editable schedule form (interval, start, day-of-month, time window, timezone), validates inputs, notifies user and refreshes schedule. |\n\n## Sequence Diagram(s)\n```mermaid\nsequenceDiagram\n  autonumber\n  actor User\n  participant UI as Browser Modal\n  participant InfoStore as Global Info Store\n  participant API as Backend\n\n  User->>UI: Open Duplication Modal\n  UI->>InfoStore: read recordingId\n  alt recordingId present\n    UI->>API: getRecording(recordingId)\n    API-->>UI: recording (workflow, meta)\n    UI->>UI: derive targetUrl from last \"goto\"\n  else missing\n    UI->>InfoStore: notify(error)\n  end\n\n  User->>UI: Save (targetUrl)\n  UI->>API: duplicateRecording(recording_meta.id, targetUrl)\n  API-->>UI: success / failure\n  alt success\n    UI->>InfoStore: setRerenderRobots(true)\n    UI->>InfoStore: notify(success)\n    UI-->>User: close modal, invoke start handler\n  else failure\n    UI->>InfoStore: notify(error)\n  end\n```\n\n```mermaid\nsequenceDiagram\n  autonumber\n  actor User\n  participant UI as Edit/Schedule Modal\n  participant API as Backend\n  participant InfoStore as Global Info Store\n\n  User->>UI: Open Edit or Schedule Modal\n  UI->>API: getRecording(recordingId) / getSchedule(id)\n  API-->>UI: recording / schedule\n  UI->>UI: parse credentials, limits, targetUrl / populate schedule form\n\n  User->>UI: Modify fields\n  User->>UI: Save\n  UI->>API: updateRecording(...) / saveSchedule(...)\n  API-->>UI: success / failure\n  alt success\n    UI->>InfoStore: setRerenderRobots(true)\n    UI->>InfoStore: notify(success)\n    UI-->>User: close modal\n  else failure\n    UI->>InfoStore: notify(error)\n  end\n```\n\n## Estimated code review effort\n🎯 4 (Complex) | ⏱️ ~60 minutes\n\n## Possibly related PRs\n- getmaxun/maxun#309 — Adds duplication handler/menu entry in RecordingsTable that likely invokes the new RobotDuplicationModal.\n- getmaxun/maxun#427 — Changes robot UI wiring to use `setRerenderRobots`/rerender mechanism used by these modals.\n- getmaxun/maxun#719 — Implements overlapping robot configuration UIs (duplicate/edit/settings) as pages, overlapping feature set and control flow.\n\n## Suggested labels\n`Type: Chore`, `Scope: UI/UX`\n\n## Poem\n> I nibble keys and hop with glee,  \n> Four little modals sprout from me.  \n> I fetch a URL, tweak it neat,  \n> Save, notify, then skip a beat.  \n> Robots hum — I thump, carrot treat. 🥕\n\n<!-- walkthrough_end -->\n\n\n<!-- pre_merge_checks_walkthrough_start -->\n\n## Pre-merge checks and finishing touches\n<details>\n<summary>✅ Passed checks (3 passed)</summary>\n\n|     Check name     | Status   | Explanation                                                                                                                                                       |\n| :----------------: | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------- |\n|  Description Check | ✅ Passed | Check skipped - CodeRabbit’s high-level summary is enabled.                                                                                                       |\n|     Title Check    | ✅ Passed | The title accurately describes the main change—relocating robot modal components into the legacy directory—and is concise and specific without extraneous detail. |\n| Docstring Coverage | ✅ Passed | No functions found in the changes. Docstring coverage check skipped.                                                                                              |\n\n</details>\n\n<!-- pre_merge_checks_walkthrough_end -->\n\n<!-- finishing_touch_checkbox_start -->\n\n<details>\n<summary>✨ Finishing touches</summary>\n\n- [ ] <!-- {\"checkboxId\": \"7962f53c-55bc-4827-bfbf-6a18da830691\"} --> 📝 Generate docstrings\n<details>\n<summary>🧪 Generate unit tests (beta)</summary>\n\n- [ ] <!-- {\"checkboxId\": \"f47ac10b-58cc-4372-a567-0e02b2c3d479\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Create PR with unit tests\n- [ ] <!-- {\"checkboxId\": \"07f1e7d6-8a8e-4e23-9900-8731c2c87f58\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Post copyable unit tests in a comment\n- [ ] <!-- {\"checkboxId\": \"6ba7b810-9dad-11d1-80b4-00c04fd430c8\", \"radioGroupId\": \"utg-output-choice-group-unknown_comment_id\"} -->   Commit unit tests in branch `legacy-robs`\n\n</details>\n\n</details>\n\n<!-- finishing_touch_checkbox_end -->\n\n<!-- tips_start -->\n\n---\n\nThanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.\n\n<details>\n<summary>❤️ Share</summary>\n\n- [X](https://twitter.com/intent/tweet?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A&url=https%3A//coderabbit.ai)\n- [Mastodon](https://mastodon.social/share?text=I%20just%20used%20%40coderabbitai%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20the%20proprietary%20code.%20Check%20it%20out%3A%20https%3A%2F%2Fcoderabbit.ai)\n- [Reddit](https://www.reddit.com/submit?title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&text=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code.%20Check%20it%20out%3A%20https%3A//coderabbit.ai)\n- [LinkedIn](https://www.linkedin.com/sharing/share-offsite/?url=https%3A%2F%2Fcoderabbit.ai&mini=true&title=Great%20tool%20for%20code%20review%20-%20CodeRabbit&summary=I%20just%20used%20CodeRabbit%20for%20my%20code%20review%2C%20and%20it%27s%20fantastic%21%20It%27s%20free%20for%20OSS%20and%20offers%20a%20free%20trial%20for%20proprietary%20code)\n\n</details>\n\n<sub>Comment `@coderabbitai help` to get the list of available commands and usage tips.</sub>\n\n<!-- tips_end -->\n\n<!-- internal state start -->\n\n\n<!-- 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 -->\n\n<!-- internal state end -->"},"request":{"retryCount":1}},"response":{"url":"https://api.github.com/repos/getmaxun/maxun/issues/comments/3371428944","status":404,"headers":{"access-control-allow-origin":"*","access-control-expose-headers":"ETag, Link, Location, Retry-After, X-GitHub-OTP, X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Used, X-RateLimit-Resource, X-RateLimit-Reset, X-OAuth-Scopes, X-Accepted-OAuth-Scopes, X-Poll-Interval, X-GitHub-Media-Type, X-GitHub-SSO, X-GitHub-Request-Id, Deprecation, Sunset","content-encoding":"gzip","content-security-policy":"default-src 'none'","content-type":"application/json; charset=utf-8","date":"Mon, 06 Oct 2025 12:56:13 GMT","referrer-policy":"origin-when-cross-origin, strict-origin-when-cross-origin","server":"github.com","strict-transport-security":"max-age=31536000; includeSubdomains; preload","transfer-encoding":"chunked","vary":"Accept-Encoding, Accept, X-Requested-With","x-accepted-github-permissions":"issues=write; pull_requests=write","x-content-type-options":"nosniff","x-frame-options":"deny","x-github-api-version-selected":"2022-11-28","x-github-media-type":"github.v3; format=json","x-github-request-id":"B0E2:EB885:144A3D0:4A8F8D4:68E3BC6D","x-ratelimit-limit":"5000","x-ratelimit-remaining":"4817","x-ratelimit-reset":"1759755640","x-ratelimit-resource":"core","x-ratelimit-used":"183","x-xss-protection":"0"},"data":{"message":"Not Found","documentation_url":"https://docs.github.com/rest/issues/comments#update-an-issue-comment","status":"404"}}}

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🧹 Nitpick comments (1)
legacy/src/ScheduleSettings.tsx (1)

113-120: Add recordingId to the dependency array.

The useEffect uses recordingId from the global store but doesn't include it in the dependency array. While recordingId from global context is likely stable, explicitly including it follows React best practices and prevents potential stale closure issues.

Apply this diff:

   useEffect(() => {
     if (isOpen) {
       const fetchSchedule = async () => {
         await getRobotSchedule();
       };
       fetchSchedule();
     }
-  }, [isOpen]);
+  }, [isOpen, recordingId]);
📜 Review details

Configuration used: CodeRabbit UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8cb7c52 and df7aadb.

📒 Files selected for processing (1)
  • legacy/src/ScheduleSettings.tsx (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
legacy/src/ScheduleSettings.tsx (5)
src/context/globalInfo.tsx (1)
  • useGlobalInfoStore (183-183)
src/api/storage.ts (2)
  • deleteSchedule (256-268)
  • getSchedule (242-254)
src/components/ui/GenericModal.tsx (1)
  • GenericModal (13-29)
src/components/ui/DropdownMui.tsx (1)
  • Dropdown (15-32)
src/constants/const.ts (1)
  • validMomentTimezones (11-604)

Comment on lines +84 to +102
const deleteRobotSchedule = () => {
if (recordingId) {
deleteSchedule(recordingId);
setSchedule(null);
notify('success', t('Schedule deleted successfully'));
} else {
console.error('No recording id provided');
}

setSettings({
runEvery: 1,
runEveryUnit: 'HOURS',
startFrom: 'MONDAY',
dayOfMonth: '',
atTimeStart: '00:00',
atTimeEnd: '01:00',
timezone: 'UTC'
});
};
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Await the async deleteSchedule call and add error handling.

The deleteSchedule function is async but not awaited (line 86). This can cause race conditions where the state is reset and success notification is shown before the deletion completes or even if it fails.

Apply this diff to fix the issue:

-  const deleteRobotSchedule = () => {
+  const deleteRobotSchedule = async () => {
     if (recordingId) {
-      deleteSchedule(recordingId);
-      setSchedule(null);
-      notify('success', t('Schedule deleted successfully'));
+      const success = await deleteSchedule(recordingId);
+      if (success) {
+        setSchedule(null);
+        notify('success', t('Schedule deleted successfully'));
+        setSettings({
+          runEvery: 1,
+          runEveryUnit: 'HOURS',
+          startFrom: 'MONDAY',
+          dayOfMonth: '',
+          atTimeStart: '00:00',
+          atTimeEnd: '01:00',
+          timezone: 'UTC'
+        });
+      } else {
+        notify('error', t('Failed to delete schedule'));
+      }
     } else {
       console.error('No recording id provided');
     }
-
-    setSettings({
-      runEvery: 1,
-      runEveryUnit: 'HOURS',
-      startFrom: 'MONDAY',
-      dayOfMonth: '',
-      atTimeStart: '00:00',
-      atTimeEnd: '01:00',
-      timezone: 'UTC'
-    });
   };
📝 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
const deleteRobotSchedule = () => {
if (recordingId) {
deleteSchedule(recordingId);
setSchedule(null);
notify('success', t('Schedule deleted successfully'));
} else {
console.error('No recording id provided');
}
setSettings({
runEvery: 1,
runEveryUnit: 'HOURS',
startFrom: 'MONDAY',
dayOfMonth: '',
atTimeStart: '00:00',
atTimeEnd: '01:00',
timezone: 'UTC'
});
};
const deleteRobotSchedule = async () => {
if (recordingId) {
const success = await deleteSchedule(recordingId);
if (success) {
setSchedule(null);
notify('success', t('Schedule deleted successfully'));
setSettings({
runEvery: 1,
runEveryUnit: 'HOURS',
startFrom: 'MONDAY',
dayOfMonth: '',
atTimeStart: '00:00',
atTimeEnd: '01:00',
timezone: 'UTC'
});
} else {
notify('error', t('Failed to delete schedule'));
}
} else {
console.error('No recording id provided');
}
};
🤖 Prompt for AI Agents
In legacy/src/ScheduleSettings.tsx around lines 84 to 102, the
deleteRobotSchedule handler calls the async deleteSchedule without awaiting it
and lacks error handling; change the function to be async, await
deleteSchedule(recordingId) inside a try/catch, only call setSchedule(null) and
notify('success', ...) after the awaited deletion succeeds, and in the catch
call notify('error', ...) (and optionally console.error) with the error message;
preserve the final settings reset either in finally or after success as
appropriate.

{schedule !== null ? (
<>
<Typography>{t('schedule_settings.run_every')}: {schedule.runEvery} {schedule.runEveryUnit.toLowerCase()}</Typography>
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant ternary operator.

The ternary operator evaluates to the same translation key regardless of condition. This is likely a copy-paste error or incomplete implementation.

Apply this diff to simplify:

-              <Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
+              <Typography>{t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
📝 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
<Typography>{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.start_from') : t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
<Typography>{t('schedule_settings.start_from')}: {schedule.startFrom.charAt(0).toUpperCase() + schedule.startFrom.slice(1).toLowerCase()}</Typography>
🤖 Prompt for AI Agents
In legacy/src/ScheduleSettings.tsx around line 159, the ternary operator around
the translation call is redundant because both branches return the same key;
replace the entire ternary expression with a single call to
t('schedule_settings.start_from') so the JSX becomes a single Typography that
uses t('schedule_settings.start_from') followed by the formatted
schedule.startFrom value (preserve the existing capitalization logic).

Comment on lines +199 to +201
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')}
</Typography>
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Remove redundant ternary operator.

The ternary operator evaluates to the same translation key regardless of condition. This appears to be duplicate code from line 159.

Apply this diff to simplify:

                 <Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
-                  {['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')}
+                  {t('schedule_settings.labels.start_from_label')}
                 </Typography>
📝 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
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
{['MONTHS', 'WEEKS'].includes(settings.runEveryUnit) ? t('schedule_settings.labels.start_from_label') : t('schedule_settings.labels.start_from_label')}
</Typography>
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>
{t('schedule_settings.labels.start_from_label')}
</Typography>
🤖 Prompt for AI Agents
In legacy/src/ScheduleSettings.tsx around lines 199 to 201, the JSX uses a
ternary that returns the same translation key for both branches; remove the
redundant conditional and replace the entire expression with a single call to
t('schedule_settings.labels.start_from_label') so the Typography simply renders
that translation.

Comment on lines +269 to +271
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz.charAt(0).toUpperCase() + tz.slice(1).toLowerCase()}</MenuItem>
))}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

Timezone display may not capitalize correctly.

The capitalization logic tz.charAt(0).toUpperCase() + tz.slice(1).toLowerCase() will convert "America/New_York" to "America/new_york", which is incorrect. Most timezone names in validMomentTimezones contain slashes and underscores that should be preserved.

Consider displaying timezones as-is or using a more sophisticated formatting approach:

                   {validMomentTimezones.map((tz) => (
-                    <MenuItem key={tz} value={tz}>{tz.charAt(0).toUpperCase() + tz.slice(1).toLowerCase()}</MenuItem>
+                    <MenuItem key={tz} value={tz}>{tz}</MenuItem>
                   ))}
📝 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
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz.charAt(0).toUpperCase() + tz.slice(1).toLowerCase()}</MenuItem>
))}
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
))}
🤖 Prompt for AI Agents
In legacy/src/ScheduleSettings.tsx around lines 269-271 the current
capitalization logic corrupts timezone names (e.g. "America/New_York" ->
"America/new_york"); replace the inline transform with a proper formatter:
either render the timezone string as-is, or implement a small formatter that
splits on '/' and for each segment replaces '_' with ' ', capitalizes each word
(first letter only) and then joins segments with " / " so "America/New_York"
becomes "America / New York"; keep the MenuItem key/value as the original tz.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant