feat: resolve merge conflicts for browser recording save
This commit is contained in:
195
src/components/robot/Recordings.tsx
Normal file
195
src/components/robot/Recordings.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
460
src/components/robot/RecordingsTable.tsx
Normal file
460
src/components/robot/RecordingsTable.tsx
Normal 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',
|
||||
};
|
||||
175
src/components/robot/RobotDuplicate.tsx
Normal file
175
src/components/robot/RobotDuplicate.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
197
src/components/robot/RobotEdit.tsx
Normal file
197
src/components/robot/RobotEdit.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
162
src/components/robot/RobotSettings.tsx
Normal file
162
src/components/robot/RobotSettings.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
307
src/components/robot/ScheduleSettings.tsx
Normal file
307
src/components/robot/ScheduleSettings.tsx
Normal 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',
|
||||
};
|
||||
61
src/components/robot/ToggleButton.tsx
Normal file
61
src/components/robot/ToggleButton.tsx
Normal 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;
|
||||
}
|
||||
}
|
||||
`;
|
||||
Reference in New Issue
Block a user