feat: resolve merge conflicts for browser recording save

This commit is contained in:
RohitR311
2025-01-10 11:50:36 +05:30
97 changed files with 4093 additions and 1589 deletions

View File

@@ -0,0 +1,195 @@
import React, { useState } from 'react';
import { RecordingsTable } from "./RecordingsTable";
import { Grid } from "@mui/material";
import { RunSettings, RunSettingsModal } from "../run/RunSettings";
import { ScheduleSettings, ScheduleSettingsModal } from "./ScheduleSettings";
import { IntegrationSettings, IntegrationSettingsModal } from "../integration/IntegrationSettings";
import { RobotSettings, RobotSettingsModal } from "./RobotSettings";
import { RobotEditModal } from './RobotEdit';
import { RobotDuplicationModal } from './RobotDuplicate';
interface RecordingsProps {
handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (settings: RunSettings) => void;
handleScheduleRecording: (settings: ScheduleSettings) => void;
setRecordingInfo: (id: string, name: string) => void;
}
export const Recordings = ({ handleEditRecording, handleRunRecording, setRecordingInfo, handleScheduleRecording }: RecordingsProps) => {
const [runSettingsAreOpen, setRunSettingsAreOpen] = useState(false);
const [scheduleSettingsAreOpen, setScheduleSettingsAreOpen] = useState(false);
const [integrateSettingsAreOpen, setIntegrateSettingsAreOpen] = useState(false);
const [robotSettingsAreOpen, setRobotSettingsAreOpen] = useState(false);
const [robotEditAreOpen, setRobotEditAreOpen] = useState(false);
const [robotDuplicateAreOpen, setRobotDuplicateAreOpen] = useState(false);
const [params, setParams] = useState<string[]>([]);
const [selectedRecordingId, setSelectedRecordingId] = useState<string>('');
const handleIntegrateRecording = (id: string, settings: IntegrationSettings) => { };
const handleSettingsRecording = (id: string, settings: RobotSettings) => { };
const handleEditRobot = (id: string, settings: RobotSettings) => { };
const handleDuplicateRobot = (id: string, settings: RobotSettings) => { };
const handleSettingsAndIntegrate = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setIntegrateSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setIntegrateSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleSettingsAndRun = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRunSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRunSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleSettingsAndSchedule = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setScheduleSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setScheduleSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleRobotSettings = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotSettingsAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleEditRobotOption = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotEditAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotEditAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleDuplicateRobotOption = (id: string, name: string, params: string[]) => {
if (params.length === 0) {
setRobotDuplicateAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
} else {
setParams(params);
setRobotDuplicateAreOpen(true);
setRecordingInfo(id, name);
setSelectedRecordingId(id);
}
}
const handleClose = () => {
setParams([]);
setRunSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleIntegrateClose = () => {
setParams([]);
setIntegrateSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleScheduleClose = () => {
setParams([]);
setScheduleSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotSettingsClose = () => {
setParams([]);
setRobotSettingsAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotEditClose = () => {
setParams([]);
setRobotEditAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
const handleRobotDuplicateClose = () => {
setParams([]);
setRobotDuplicateAreOpen(false);
setRecordingInfo('', '');
setSelectedRecordingId('');
}
return (
<React.Fragment>
<RunSettingsModal isOpen={runSettingsAreOpen}
handleClose={handleClose}
handleStart={(settings) => handleRunRecording(settings)}
isTask={params.length !== 0}
params={params}
/>
<ScheduleSettingsModal isOpen={scheduleSettingsAreOpen}
handleClose={handleScheduleClose}
handleStart={(settings) => handleScheduleRecording(settings)}
/>
<IntegrationSettingsModal isOpen={integrateSettingsAreOpen}
handleClose={handleIntegrateClose}
handleStart={(settings) => handleIntegrateRecording(selectedRecordingId, settings)}
/>
<RobotSettingsModal isOpen={robotSettingsAreOpen}
handleClose={handleRobotSettingsClose}
handleStart={(settings) => handleSettingsRecording(selectedRecordingId, settings)}
/>
<RobotEditModal isOpen={robotEditAreOpen}
handleClose={handleRobotEditClose}
handleStart={(settings) => handleEditRobot(selectedRecordingId, settings)}
/>
<RobotDuplicationModal isOpen={robotDuplicateAreOpen}
handleClose={handleRobotDuplicateClose}
handleStart={(settings) => handleDuplicateRobot(selectedRecordingId, settings)}
/>
<Grid container direction="column" sx={{ padding: '30px' }}>
<Grid item xs>
<RecordingsTable
handleEditRecording={handleEditRecording}
handleRunRecording={handleSettingsAndRun}
handleScheduleRecording={handleSettingsAndSchedule}
handleIntegrateRecording={handleSettingsAndIntegrate}
handleSettingsRecording={handleRobotSettings}
handleEditRobot={handleEditRobotOption}
handleDuplicateRobot={handleDuplicateRobotOption}
/>
</Grid>
</Grid>
</React.Fragment>
);
}

View File

@@ -0,0 +1,460 @@
import * as React from 'react';
import { useTranslation } from 'react-i18next';
import Paper from '@mui/material/Paper';
import Table from '@mui/material/Table';
import TableBody from '@mui/material/TableBody';
import TableCell from '@mui/material/TableCell';
import TableContainer from '@mui/material/TableContainer';
import TableHead from '@mui/material/TableHead';
import TablePagination from '@mui/material/TablePagination';
import TableRow from '@mui/material/TableRow';
import { useEffect } from "react";
import { WorkflowFile } from "maxun-core";
import SearchIcon from '@mui/icons-material/Search';
import { IconButton, Button, Box, Typography, TextField, MenuItem, Menu, ListItemIcon, ListItemText } from "@mui/material";
import { Schedule, DeleteForever, Edit, PlayCircle, Settings, Power, ContentCopy, MoreHoriz } from "@mui/icons-material";
import { useGlobalInfoStore } from "../../context/globalInfo";
import { checkRunsForRecording, deleteRecordingFromStorage, getStoredRecordings } from "../../api/storage";
import { Add } from "@mui/icons-material";
import { useNavigate } from 'react-router-dom';
import { stopRecording } from "../../api/recording";
import { GenericModal } from '../ui/GenericModal';
/** TODO:
* 1. allow editing existing robot after persisting browser steps
*/
interface Column {
id: 'interpret' | 'name' | 'options' | 'schedule' | 'integrate' | 'settings';
label: string;
minWidth?: number;
align?: 'right';
format?: (value: string) => string;
}
interface Data {
id: string;
name: string;
createdAt: string;
updatedAt: string;
content: WorkflowFile;
params: string[];
}
interface RecordingsTableProps {
handleEditRecording: (id: string, fileName: string) => void;
handleRunRecording: (id: string, fileName: string, params: string[]) => void;
handleScheduleRecording: (id: string, fileName: string, params: string[]) => void;
handleIntegrateRecording: (id: string, fileName: string, params: string[]) => void;
handleSettingsRecording: (id: string, fileName: string, params: string[]) => void;
handleEditRobot: (id: string, name: string, params: string[]) => void;
handleDuplicateRobot: (id: string, name: string, params: string[]) => void;
}
export const RecordingsTable = ({ handleEditRecording, handleRunRecording, handleScheduleRecording, handleIntegrateRecording, handleSettingsRecording, handleEditRobot, handleDuplicateRobot }: RecordingsTableProps) => {
const { t } = useTranslation();
const [page, setPage] = React.useState(0);
const [rowsPerPage, setRowsPerPage] = React.useState(10);
const [rows, setRows] = React.useState<Data[]>([]);
const [isModalOpen, setModalOpen] = React.useState(false);
const [searchTerm, setSearchTerm] = React.useState('');
const columns: readonly Column[] = [
{ id: 'interpret', label: t('recordingtable.run'), minWidth: 80 },
{ id: 'name', label: t('recordingtable.name'), minWidth: 80 },
{
id: 'schedule',
label: t('recordingtable.schedule'),
minWidth: 80,
},
{
id: 'integrate',
label: t('recordingtable.integrate'),
minWidth: 80,
},
{
id: 'settings',
label: t('recordingtable.settings'),
minWidth: 80,
},
{
id: 'options',
label: t('recordingtable.options'),
minWidth: 80,
},
];
const { notify, setRecordings, browserId, setBrowserId, setInitialUrl, recordingUrl, setRecordingUrl, recordingName, setRecordingName, recordingId, setRecordingId } = useGlobalInfoStore();
const navigate = useNavigate();
const handleChangePage = (event: unknown, newPage: number) => {
setPage(newPage);
};
const handleChangeRowsPerPage = (event: React.ChangeEvent<HTMLInputElement>) => {
setRowsPerPage(+event.target.value);
setPage(0);
};
const handleSearchChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSearchTerm(event.target.value);
setPage(0);
};
const fetchRecordings = async () => {
const recordings = await getStoredRecordings();
if (recordings) {
const parsedRows: Data[] = [];
recordings.map((recording: any, index: number) => {
if (recording && recording.recording_meta) {
parsedRows.push({
id: index,
...recording.recording_meta,
content: recording.recording
});
}
});
setRecordings(parsedRows.map((recording) => recording.name));
setRows(parsedRows);
} else {
console.log('No recordings found.');
}
}
const handleNewRecording = async () => {
if (browserId) {
setBrowserId(null);
await stopRecording(browserId);
}
setModalOpen(true);
};
const handleStartRecording = () => {
setBrowserId('new-recording');
setRecordingName('');
setRecordingId('');
navigate('/recording');
}
const startRecording = () => {
setModalOpen(false);
handleStartRecording();
};
const setBrowserRecordingUrl = (event: React.ChangeEvent<HTMLInputElement>) => {
setInitialUrl(event.target.value);
setRecordingUrl(event.target.value);
}
useEffect(() => {
if (rows.length === 0) {
fetchRecordings();
}
}, []);
// Filter rows based on search term
const filteredRows = rows.filter((row) =>
row.name.toLowerCase().includes(searchTerm.toLowerCase())
);
return (
<React.Fragment>
<Box display="flex" justifyContent="space-between" alignItems="center">
<Typography variant="h6" gutterBottom>
{t('recordingtable.heading')}
</Typography>
<Box display="flex" alignItems="center" gap={2}>
<TextField
size="small"
placeholder={t('recordingtable.search')}
value={searchTerm}
onChange={handleSearchChange}
InputProps={{
startAdornment: <SearchIcon sx={{ color: 'action.active', mr: 1 }} />
}}
sx={{ width: '250px' }}
/>
<IconButton
aria-label="new"
size={"small"}
onClick={handleNewRecording}
sx={{
width: '140px',
borderRadius: '5px',
padding: '8px',
background: '#ff00c3',
color: 'white',
marginRight: '10px',
fontFamily: '"Roboto","Helvetica","Arial",sans-serif',
fontWeight: '500',
fontSize: '0.875rem',
lineHeight: '1.75',
letterSpacing: '0.02857em',
'&:hover': { color: 'white', backgroundColor: '#ff00c3' }
}}
>
<Add sx={{ marginRight: '5px' }} /> {t('recordingtable.new')}
</IconButton>
</Box>
</Box>
<TableContainer component={Paper} sx={{ width: '100%', overflow: 'hidden', marginTop: '15px' }}>
<Table stickyHeader aria-label="sticky table">
<TableHead>
<TableRow>
{columns.map((column) => (
<TableCell
key={column.id}
align={column.align}
style={{ minWidth: column.minWidth }}
>
{column.label}
</TableCell>
))}
</TableRow>
</TableHead>
<TableBody>
{filteredRows.length !== 0 ? filteredRows
.slice(page * rowsPerPage, page * rowsPerPage + rowsPerPage)
.map((row) => {
return (
<TableRow hover role="checkbox" tabIndex={-1} key={row.id}>
{columns.map((column) => {
// @ts-ignore
const value: any = row[column.id];
if (value !== undefined) {
return (
<TableCell key={column.id} align={column.align}>
{value}
</TableCell>
);
} else {
switch (column.id) {
case 'interpret':
return (
<TableCell key={column.id} align={column.align}>
<InterpretButton handleInterpret={() => handleRunRecording(row.id, row.name, row.params || [])} />
</TableCell>
);
case 'schedule':
return (
<TableCell key={column.id} align={column.align}>
<ScheduleButton handleSchedule={() => handleScheduleRecording(row.id, row.name, row.params || [])} />
</TableCell>
);
case 'integrate':
return (
<TableCell key={column.id} align={column.align}>
<IntegrateButton handleIntegrate={() => handleIntegrateRecording(row.id, row.name, row.params || [])} />
</TableCell>
);
case 'options':
return (
<TableCell key={column.id} align={column.align}>
<OptionsButton
handleEdit={() => handleEditRobot(row.id, row.name, row.params || [])}
handleDuplicate={() => {
handleDuplicateRobot(row.id, row.name, row.params || []);
}}
handleDelete={() => {
checkRunsForRecording(row.id).then((result: boolean) => {
if (result) {
notify('warning', t('recordingtable.notifications.delete_warning'));
}
})
deleteRecordingFromStorage(row.id).then((result: boolean) => {
if (result) {
setRows([]);
notify('success', t('recordingtable.notifications.delete_success'));
fetchRecordings();
}
})
}}
/>
</TableCell>
);
case 'settings':
return (
<TableCell key={column.id} align={column.align}>
<SettingsButton handleSettings={() => handleSettingsRecording(row.id, row.name, row.params || [])} />
</TableCell>
);
default:
return null;
}
}
})}
</TableRow>
);
})
: null}
</TableBody>
</Table>
</TableContainer>
<TablePagination
rowsPerPageOptions={[10, 25, 50]}
component="div"
count={filteredRows.length}
rowsPerPage={rowsPerPage}
page={page}
onPageChange={handleChangePage}
onRowsPerPageChange={handleChangeRowsPerPage}
/>
<GenericModal isOpen={isModalOpen} onClose={() => setModalOpen(false)} modalStyle={modalStyle}>
<div style={{ padding: '20px' }}>
<Typography variant="h6" gutterBottom>{t('recordingtable.modal.title')}</Typography>
<TextField
label={t('recordingtable.modal.label')}
variant="outlined"
fullWidth
value={recordingUrl}
onChange={setBrowserRecordingUrl}
style={{ marginBottom: '20px', marginTop: '20px' }}
/>
<Button
variant="contained"
color="primary"
onClick={startRecording}
disabled={!recordingUrl}
>
{t('recordingtable.modal.button')}
</Button>
</div>
</GenericModal>
</React.Fragment>
);
}
interface InterpretButtonProps {
handleInterpret: () => void;
}
const InterpretButton = ({ handleInterpret }: InterpretButtonProps) => {
return (
<IconButton aria-label="add" size="small" onClick={() => {
handleInterpret();
}}
>
<PlayCircle />
</IconButton>
)
}
interface ScheduleButtonProps {
handleSchedule: () => void;
}
const ScheduleButton = ({ handleSchedule }: ScheduleButtonProps) => {
return (
<IconButton aria-label="add" size="small" onClick={() => {
handleSchedule();
}}
>
<Schedule />
</IconButton>
)
}
interface IntegrateButtonProps {
handleIntegrate: () => void;
}
const IntegrateButton = ({ handleIntegrate }: IntegrateButtonProps) => {
return (
<IconButton aria-label="add" size="small" onClick={() => {
handleIntegrate();
}}
>
<Power />
</IconButton>
)
}
interface SettingsButtonProps {
handleSettings: () => void;
}
const SettingsButton = ({ handleSettings }: SettingsButtonProps) => {
return (
<IconButton aria-label="add" size="small" onClick={() => {
handleSettings();
}}
>
<Settings />
</IconButton>
)
}
interface OptionsButtonProps {
handleEdit: () => void;
handleDelete: () => void;
handleDuplicate: () => void;
}
const OptionsButton = ({ handleEdit, handleDelete, handleDuplicate }: OptionsButtonProps) => {
const [anchorEl, setAnchorEl] = React.useState<null | HTMLElement>(null);
const handleClick = (event: React.MouseEvent<HTMLElement>) => {
setAnchorEl(event.currentTarget);
};
const handleClose = () => {
setAnchorEl(null);
};
const { t } = useTranslation();
return (
<>
<IconButton
aria-label="options"
size="small"
onClick={handleClick}
>
<MoreHoriz />
</IconButton>
<Menu
anchorEl={anchorEl}
open={Boolean(anchorEl)}
onClose={handleClose}
>
<MenuItem onClick={() => { handleEdit(); handleClose(); }}>
<ListItemIcon>
<Edit fontSize="small" />
</ListItemIcon>
<ListItemText>{t('recordingtable.edit')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => { handleDelete(); handleClose(); }}>
<ListItemIcon>
<DeleteForever fontSize="small" />
</ListItemIcon>
<ListItemText>{t('recordingtable.delete')}</ListItemText>
</MenuItem>
<MenuItem onClick={() => { handleDuplicate(); handleClose(); }}>
<ListItemIcon>
<ContentCopy fontSize="small" />
</ListItemIcon>
<ListItemText>{t('recordingtable.duplicate')}</ListItemText>
</MenuItem>
</Menu>
</>
);
};
const modalStyle = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '30%',
backgroundColor: 'background.paper',
p: 4,
height: 'fit-content',
display: 'block',
padding: '20px',
};

View File

@@ -0,0 +1,175 @@
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 { getUserById } from "../../api/auth";
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 [robot, setRobot] = useState<RobotSettings | null>(null);
const [targetUrl, setTargetUrl] = useState<string | undefined>('');
const { recordingId, notify } = 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);
};
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) {
notify('success', t('robot_duplication.notifications.duplicate_success'));
handleStart(robot);
handleClose();
setTimeout(() => {
window.location.reload();
}, 1000);
} 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 />
<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>
);
};

View File

@@ -0,0 +1,197 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box, Button } from "@mui/material";
import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording, updateRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
interface RobotMeta {
name: string;
id: string;
createdAt: string;
pairs: number;
updatedAt: string;
params: any[];
}
interface RobotWorkflow {
workflow: WhereWhatPair[];
}
interface RobotEditOptions {
name: string;
limit?: number;
}
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 RobotEditModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
const { t } = useTranslation();
const [robot, setRobot] = useState<RobotSettings | null>(null);
const { recordingId, notify } = useGlobalInfoStore();
useEffect(() => {
if (isOpen) {
getRobot();
}
}, [isOpen]);
const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
setRobot(robot);
} else {
notify('error', t('robot_edit.notifications.update_failed'));
}
}
const handleRobotNameChange = (newName: string) => {
setRobot((prev) =>
prev ? { ...prev, recording_meta: { ...prev.recording_meta, name: newName } } : prev
);
};
const handleLimitChange = (newLimit: number) => {
setRobot((prev) => {
if (!prev) return prev;
const updatedWorkflow = [...prev.recording.workflow];
if (
updatedWorkflow.length > 0 &&
updatedWorkflow[0]?.what &&
updatedWorkflow[0].what.length > 0 &&
updatedWorkflow[0].what[0].args &&
updatedWorkflow[0].what[0].args.length > 0 &&
updatedWorkflow[0].what[0].args[0]
) {
updatedWorkflow[0].what[0].args[0].limit = newLimit;
}
return { ...prev, recording: { ...prev.recording, workflow: updatedWorkflow } };
});
};
const handleSave = async () => {
if (!robot) return;
try {
const payload = {
name: robot.recording_meta.name,
limit: robot.recording.workflow[0]?.what[0]?.args?.[0]?.limit,
};
const success = await updateRecording(robot.recording_meta.id, payload);
if (success) {
notify('success', t('robot_edit.notifications.update_success'));
handleStart(robot); // Inform parent about the updated robot
handleClose();
setTimeout(() => {
window.location.reload();
}, 1000);
} else {
notify('error', t('robot_edit.notifications.update_failed'));
}
} catch (error) {
notify('error', t('robot_edit.notifications.update_error'));
console.error('Error updating robot:', error);
}
};
return (
<GenericModal
isOpen={isOpen}
onClose={handleClose}
modalStyle={modalStyle}
>
<>
<Typography variant="h5" style={{ marginBottom: '20px' }}>
{t('robot_edit.title')}
</Typography>
<Box style={{ display: 'flex', flexDirection: 'column' }}>
{
robot && (
<>
<TextField
label={t('robot_edit.change_name')}
key="Robot Name"
type='text'
value={robot.recording_meta.name}
onChange={(e) => handleRobotNameChange(e.target.value)}
style={{ marginBottom: '20px' }}
/>
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
<TextField
label={t('robot_edit.robot_limit')}
type="number"
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
onChange={(e) => {
const value = parseInt(e.target.value, 10);
if (value >= 1) {
handleLimitChange(value);
}
}}
inputProps={{ min: 1 }}
style={{ marginBottom: '20px' }}
/>
)}
<Box mt={2} display="flex" justifyContent="flex-end">
<Button variant="contained" color="primary" onClick={handleSave}>
{t('robot_edit.save')}
</Button>
<Button
onClick={handleClose}
color="primary"
variant="outlined"
style={{ marginLeft: '10px' }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('robot_edit.cancel')}
</Button>
</Box>
</>
)
}
</Box>
</>
</GenericModal>
);
};

View File

@@ -0,0 +1,162 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { GenericModal } from "../ui/GenericModal";
import { TextField, Typography, Box } from "@mui/material";
import { modalStyle } from "../recorder/AddWhereCondModal";
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getStoredRecording } from '../../api/storage';
import { WhereWhatPair } from 'maxun-core';
import { getUserById } from "../../api/auth";
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 RobotSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: RobotSettingsProps) => {
const { t } = useTranslation();
const [robot, setRobot] = useState<RobotSettings | null>(null);
const [userEmail, setUserEmail] = useState<string | null>(null);
const { recordingId, notify } = useGlobalInfoStore();
useEffect(() => {
if (isOpen) {
getRobot();
}
}, [isOpen]);
const getRobot = async () => {
if (recordingId) {
const robot = await getStoredRecording(recordingId);
setRobot(robot);
} else {
notify('error', t('robot_settings.errors.robot_not_found'));
}
}
const lastPair = robot?.recording.workflow[robot?.recording.workflow.length - 1];
// Find the `goto` action in `what` and retrieve its arguments
const targetUrl = lastPair?.what.find(action => action.action === "goto")?.args?.[0];
useEffect(() => {
const fetchUserEmail = async () => {
if (robot && robot.userId) {
const userData = await getUserById(robot.userId.toString());
if (userData && userData.user) {
setUserEmail(userData.user.email);
}
}
};
fetchUserEmail();
}, [robot?.userId]);
return (
<GenericModal
isOpen={isOpen}
onClose={handleClose}
modalStyle={modalStyle}
>
<>
<Typography variant="h5" style={{ marginBottom: '20px' }}>
{t('robot_settings.title')}
</Typography>
<Box style={{ display: 'flex', flexDirection: 'column' }}>
{
robot && (
<>
<TextField
label={t('robot_settings.target_url')}
key="Robot Target URL"
value={targetUrl}
InputProps={{
readOnly: true,
}}
style={{ marginBottom: '20px' }}
/>
<TextField
label={t('robot_settings.robot_id')}
key="Robot ID"
value={robot.recording_meta.id}
InputProps={{
readOnly: true,
}}
style={{ marginBottom: '20px' }}
/>
{robot.recording.workflow?.[0]?.what?.[0]?.args?.[0]?.limit !== undefined && (
<TextField
label={t('robot_settings.robot_limit')}
type="number"
value={robot.recording.workflow[0].what[0].args[0].limit || ''}
InputProps={{
readOnly: true,
}}
style={{ marginBottom: '20px' }}
/>
)}
<TextField
label={t('robot_settings.created_by_user')}
key="Created By User"
value={userEmail ? userEmail : ''}
InputProps={{
readOnly: true,
}}
style={{ marginBottom: '20px' }}
/>
<TextField
label={t('robot_settings.created_at')}
key="Robot Created At"
value={robot.recording_meta.createdAt}
InputProps={{
readOnly: true,
}}
style={{ marginBottom: '20px' }}
/>
</>
)
}
</Box>
</>
</GenericModal>
);
};

View File

@@ -0,0 +1,307 @@
import React, { useState, useEffect } from 'react';
import { useTranslation } from 'react-i18next';
import { GenericModal } from "../ui/GenericModal";
import { MenuItem, TextField, Typography, Box } from "@mui/material";
import { Dropdown } from "../ui/DropdownMui";
import Button from "@mui/material/Button";
import { validMomentTimezones } from '../../constants/const';
import { useGlobalInfoStore } from '../../context/globalInfo';
import { getSchedule, deleteSchedule } from '../../api/storage';
interface ScheduleSettingsProps {
isOpen: boolean;
handleStart: (settings: ScheduleSettings) => void;
handleClose: () => void;
initialSettings?: ScheduleSettings | null;
}
export interface ScheduleSettings {
runEvery: number;
runEveryUnit: string;
startFrom: string;
dayOfMonth?: string;
atTimeStart?: string;
atTimeEnd?: string;
timezone: string;
}
export const ScheduleSettingsModal = ({ isOpen, handleStart, handleClose, initialSettings }: ScheduleSettingsProps) => {
const { t } = useTranslation();
const [schedule, setSchedule] = useState<ScheduleSettings | null>(null);
const [settings, setSettings] = useState<ScheduleSettings>({
runEvery: 1,
runEveryUnit: 'HOURS',
startFrom: 'MONDAY',
dayOfMonth: '1',
atTimeStart: '00:00',
atTimeEnd: '01:00',
timezone: 'UTC'
});
useEffect(() => {
if (initialSettings) {
setSettings(initialSettings);
}
}, [initialSettings]);
const handleChange = (field: keyof ScheduleSettings, value: string | number | boolean) => {
setSettings(prev => ({ ...prev, [field]: value }));
};
const textStyle = {
width: '150px',
height: '52px',
marginRight: '10px',
};
const dropDownStyle = {
marginTop: '2px',
width: '150px',
height: '59px',
marginRight: '10px',
};
const units = [
'MINUTES',
'HOURS',
'DAYS',
'WEEKS',
'MONTHS'
];
const days = [
'MONDAY',
'TUESDAY',
'WEDNESDAY',
'THURSDAY',
'FRIDAY',
'SATURDAY',
'SUNDAY'
];
const { recordingId, notify } = useGlobalInfoStore();
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 getRobotSchedule = async () => {
if (recordingId) {
const scheduleData = await getSchedule(recordingId);
setSchedule(scheduleData);
} else {
console.error('No recording id provided');
}
}
useEffect(() => {
if (isOpen) {
const fetchSchedule = async () => {
await getRobotSchedule();
};
fetchSchedule();
}
}, [isOpen]);
const getDayOrdinal = (day: string | undefined) => {
if (!day) return '';
const lastDigit = day.slice(-1);
const lastTwoDigits = day.slice(-2);
// Special cases for 11, 12, 13
if (['11', '12', '13'].includes(lastTwoDigits)) {
return t('schedule_settings.labels.on_day.th');
}
// Other cases
switch (lastDigit) {
case '1': return t('schedule_settings.labels.on_day.st');
case '2': return t('schedule_settings.labels.on_day.nd');
case '3': return t('schedule_settings.labels.on_day.rd');
default: return t('schedule_settings.labels.on_day.th');
}
};
return (
<GenericModal
isOpen={isOpen}
onClose={handleClose}
modalStyle={modalStyle}
>
<Box sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'flex-start',
padding: '20px',
'& > *': { marginBottom: '20px' },
}}>
<Typography variant="h6" sx={{ marginBottom: '20px' }}>{t('schedule_settings.title')}</Typography>
<>
{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>
{schedule.runEveryUnit === 'MONTHS' && (
<Typography>{t('schedule_settings.on_day')}: {schedule.dayOfMonth}{getDayOrdinal(schedule.dayOfMonth)} of the month</Typography>
)}
<Typography>{t('schedule_settings.at_around')}: {schedule.atTimeStart}, {schedule.timezone} {t('schedule_settings.timezone')}</Typography>
<Box mt={2} display="flex" justifyContent="space-between">
<Button
onClick={deleteRobotSchedule}
variant="outlined"
color="error"
>
{t('schedule_settings.buttons.delete_schedule')}
</Button>
</Box>
</>
) : (
<>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.labels.run_once_every')}</Typography>
<TextField
type="number"
value={settings.runEvery}
onChange={(e) => handleChange('runEvery', parseInt(e.target.value))}
sx={textStyle}
inputProps={{ min: 1 }}
/>
<Dropdown
label=""
id="runEveryUnit"
value={settings.runEveryUnit}
handleSelect={(e) => handleChange('runEveryUnit', e.target.value)}
sx={dropDownStyle}
>
{units.map((unit) => (
<MenuItem key={unit} value={unit}>{unit}</MenuItem>
))}
</Dropdown>
</Box>
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<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>
<Dropdown
label=""
id="startFrom"
value={settings.startFrom}
handleSelect={(e) => handleChange('startFrom', e.target.value)}
sx={dropDownStyle}
>
{days.map((day) => (
<MenuItem key={day} value={day}>{day}</MenuItem>
))}
</Dropdown>
</Box>
{settings.runEveryUnit === 'MONTHS' && (
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginBottom: '5px', marginRight: '25px' }}>{t('schedule_settings.labels.on_day_of_month')}</Typography>
<TextField
type="number"
value={settings.dayOfMonth}
onChange={(e) => handleChange('dayOfMonth', e.target.value)}
sx={textStyle}
inputProps={{ min: 1, max: 31 }}
/>
</Box>
)}
{['MINUTES', 'HOURS'].includes(settings.runEveryUnit) ? (
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Box sx={{ marginRight: '20px' }}>
<Typography sx={{ marginBottom: '5px' }}>{t('schedule_settings.labels.in_between')}</Typography>
<TextField
type="time"
value={settings.atTimeStart}
onChange={(e) => handleChange('atTimeStart', e.target.value)}
sx={textStyle}
/>
<TextField
type="time"
value={settings.atTimeEnd}
onChange={(e) => handleChange('atTimeEnd', e.target.value)}
sx={textStyle}
/>
</Box>
</Box>
) : (
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginBottom: '5px', marginRight: '10px' }}>{t('schedule_settings.at_around')}</Typography>
<TextField
type="time"
value={settings.atTimeStart}
onChange={(e) => handleChange('atTimeStart', e.target.value)}
sx={textStyle}
/>
</Box>
)}
<Box sx={{ display: 'flex', alignItems: 'center', width: '100%' }}>
<Typography sx={{ marginRight: '10px' }}>{t('schedule_settings.timezone')}</Typography>
<Dropdown
label=""
id="timezone"
value={settings.timezone}
handleSelect={(e) => handleChange('timezone', e.target.value)}
sx={dropDownStyle}
>
{validMomentTimezones.map((tz) => (
<MenuItem key={tz} value={tz}>{tz}</MenuItem>
))}
</Dropdown>
</Box>
<Box mt={2} display="flex" justifyContent="flex-end">
<Button onClick={() => handleStart(settings)} variant="contained" color="primary">
{t('schedule_settings.buttons.save_schedule')}
</Button>
<Button
onClick={handleClose}
color="primary"
variant="outlined"
style={{ marginLeft: '10px' }}
sx={{
color: '#ff00c3 !important',
borderColor: '#ff00c3 !important',
backgroundColor: 'whitesmoke !important',
}}>
{t('schedule_settings.buttons.cancel')}
</Button>
</Box>
</>
)}
</>
</Box>
</GenericModal>
);
};
const modalStyle = {
top: '50%',
left: '50%',
transform: 'translate(-50%, -50%)',
width: '40%',
backgroundColor: 'background.paper',
p: 4,
height: 'fit-content',
display: 'block',
padding: '20px',
};

View File

@@ -0,0 +1,61 @@
import React, { FC } from "react";
import styled from "styled-components";
interface ToggleButtonProps {
isChecked?: boolean;
onChange: () => void;
};
export const ToggleButton: FC<ToggleButtonProps> = ({ isChecked = false, onChange }) => (
<CheckBoxWrapper>
<CheckBox id="checkbox" type="checkbox" onClick={onChange} checked={isChecked} />
<CheckBoxLabel htmlFor="checkbox" />
</CheckBoxWrapper>
);
const CheckBoxWrapper = styled.div`
position: relative;
`;
const CheckBoxLabel = styled.label`
position: absolute;
top: 0;
left: 0;
width: 42px;
height: 26px;
border-radius: 15px;
background: #bebebe;
cursor: pointer;
&::after {
content: "";
display: block;
border-radius: 50%;
width: 18px;
height: 18px;
margin: 3px;
background: #ffffff;
box-shadow: 1px 3px 3px 1px rgba(0, 0, 0, 0.2);
transition: 0.2s;
}
`;
const CheckBox = styled.input`
opacity: 0;
z-index: 1;
border-radius: 15px;
width: 42px;
height: 26px;
&:checked + ${CheckBoxLabel} {
background: #2196F3;
&::after {
content: "";
display: block;
border-radius: 50%;
width: 18px;
height: 18px;
margin-left: 21px;
transition: 0.2s;
}
}
`;