diff --git a/public/mobile_entities.soil b/public/mobile_entities.soil index 5e3437ea0b03c8501cc49995558feddf4833bdc3..e778c69d16de48479996b68fc676822a850b51f3 100644 --- a/public/mobile_entities.soil +++ b/public/mobile_entities.soil @@ -60,6 +60,8 @@ function Trigger { returns: utils.Position position Label outputLabel + + position.description = "Position measured with explicit triggering." } component Target { diff --git a/src/app/Layout/Navigation/NavigationBar.tsx b/src/app/Layout/Navigation/NavigationBar.tsx index c0c62588b616f4d29857931c21a4ec1303acd3a0..c9206e66bcda771db0bcbcc2747a488b068acb8a 100644 --- a/src/app/Layout/Navigation/NavigationBar.tsx +++ b/src/app/Layout/Navigation/NavigationBar.tsx @@ -12,6 +12,7 @@ import PublicIcon from '@mui/icons-material/Public'; import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { drawerWidth } from './NavigationDrawer'; import { selectDarkmodeEnabled, toggleDarkmode } from './darkmodeSlice'; +import TextSnippetOutlinedIcon from '@mui/icons-material/TextSnippetOutlined'; import UserMenu from './UserMenu'; // NavigationBar component @@ -32,12 +33,15 @@ const NavigationBar = (): JSX.Element => { <Button startIcon={<MenuBookIcon />} color={'inherit'} target="_blank" href="https://doi.org/10.5281/zenodo.7757249"> Docs </Button> - <Button startIcon={<PublicIcon></PublicIcon>} component={Link} color={'inherit'} to={'/public'} disabled={true}> + <Button startIcon={<PublicIcon></PublicIcon>} component={Link} color={'inherit'} to={'/public'} disabled={false}> Public Models </Button> - <Button startIcon={<LockOpenIcon></LockOpenIcon>} component={Link} color={'inherit'} to={'/private'}> + <Button startIcon={<LockOpenIcon></LockOpenIcon>} component={Link} color={'inherit'} to={'/private'} disabled={false}> Private Models </Button> + <Button startIcon={<TextSnippetOutlinedIcon></TextSnippetOutlinedIcon>} component={Link} color={'inherit'} to={'/demo'} disabled={false}> + Demo Models + </Button> {/* Spacer for alignment */} <Box sx={{ flexGrow: 1 }} /> {/* <img src={AWKLogo} alt={'AWK logo'} style={{ width: drawerWidth, marginRight: "2rem" }} /> */} diff --git a/src/app/Layout/Navigation/NavigationDrawer.tsx b/src/app/Layout/Navigation/NavigationDrawer.tsx index 6c9e3606d22d52288779e9d117546fec2f8ba1f4..6ff327ca7cb8ae43ab7d8791844748a87653bb23 100644 --- a/src/app/Layout/Navigation/NavigationDrawer.tsx +++ b/src/app/Layout/Navigation/NavigationDrawer.tsx @@ -39,7 +39,7 @@ const NavigationDrawer = (): JSX.Element => { setDialogOpen(temp) }; - // Fetch the list of soil projects + // Fetch the list of local soil projects const soilProjects = useAppSelector(selectProjects); return ( @@ -60,7 +60,7 @@ const NavigationDrawer = (): JSX.Element => { <AccountTreeIcon /> </ListItemIcon> <ListItemText> - Projects + Local Projects </ListItemText> {open ? <ExpandLess /> : <ExpandMore />} </ListItem> @@ -71,9 +71,8 @@ const NavigationDrawer = (): JSX.Element => { Object.keys(soilProjects).map((value, index) => { return <div key={"ListProject" + value}> {/* Delete file dialog */} - <DeleteFileDialog open={!!dialogOpen[index]} setOpen={(value: boolean) => { handleDialog(index) }} filename={soilProjects[value].name} deleteFile={(filename: string) => + <DeleteFileDialog open={!!dialogOpen[index]} setOpen={(value: boolean) => { handleDialog(index) }} filename={soilProjects[value].name} project={soilProjects[value]} deleteFile={(filename: string) => { dispatch(removeProject(value)); - if (usertoken.logged_in && usertoken.token) dispatch(deleteProjectById(value)); }} /> {/* Project list item */} <ListItem secondaryAction={ diff --git a/src/app/Layout/Navigation/UserMenu.tsx b/src/app/Layout/Navigation/UserMenu.tsx index 7830a3c7ba2c5b089ac691a68cd4c47165028bd8..8b685144adf720713726757c0d671a21b15a750f 100644 --- a/src/app/Layout/Navigation/UserMenu.tsx +++ b/src/app/Layout/Navigation/UserMenu.tsx @@ -5,7 +5,7 @@ import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { logout, usertokenState } from './usertokenSlice'; import UserTokenDialog from './UserTokenDialog'; import JSZip from 'jszip'; -import { selectProjects } from '../../../features/soil-editor/soileditorSlice'; +import { clearPrivateProjects, selectProjects } from '../../../features/soil-editor/soileditorSlice'; import FileSaver from 'file-saver'; const UserMenu = (): JSX.Element => { @@ -72,7 +72,10 @@ const UserMenu = (): JSX.Element => { <>Login</> )} </MenuItem> - {usertoken.logged_in && <MenuItem onClick={() => {dispatch(logout())}}>Logout</MenuItem>} + {usertoken.logged_in && <MenuItem onClick={() => { + dispatch(logout()); + dispatch(clearPrivateProjects()); + }}>Logout</MenuItem>} {usertoken.logged_in && <MenuItem onClick={() => {downloadAllProjects()}}>Download all projects</MenuItem>} <UserTokenDialog open={open} onClose={()=>{setOpen(false)}}></UserTokenDialog> </Menu> diff --git a/src/app/Layout/Navigation/usertokenSlice.tsx b/src/app/Layout/Navigation/usertokenSlice.tsx index e24d2327b6f05e31cf5d21edda4d6c5d070a50e4..f9ac038f13857365cce84c61e17fd107a03c75e7 100644 --- a/src/app/Layout/Navigation/usertokenSlice.tsx +++ b/src/app/Layout/Navigation/usertokenSlice.tsx @@ -60,7 +60,6 @@ const usertokenSlice = createSlice({ initialState: initializer(), reducers: { logout(state, action: PayloadAction<void>) { - console.log("test123"); localStorage.removeItem("token"); state.logged_in = false; state.token = null; diff --git a/src/const.ts b/src/const.ts index 198361572f8bde12ad572425e70f2a9adb1892a1..c6f44c36814dfd089f801958057773a613a40753 100644 --- a/src/const.ts +++ b/src/const.ts @@ -3,6 +3,6 @@ export const EXPERIMENTAL_MODE = true; export const DEV_MODE = process.env.NODE_ENV === 'development'; export const BASE_URL = DEV_MODE ? "http://localhost:3000" : "https://iot.wzl-mq.rwth-aachen.de/soil"; export const DATA_BACKEND = DEV_MODE ? "http://localhost:8002" : "https://iot.wzl-mq.rwth-aachen.de/soil-data" -export const SOIL_BACKEND = DEV_MODE ? "http://localhost:8001" : "https://iot.wzl-mq.rwth-aachen.de/soil-backend" -// export const SOIL_BACKEND = DEV_MODE ? "http://localhost:8001" : "http://localhost:8412" +// export const SOIL_BACKEND = DEV_MODE ? "http://localhost:8001" : "https://iot.wzl-mq.rwth-aachen.de/soil-backend" +export const SOIL_BACKEND = DEV_MODE ? "http://localhost:8001" : "http://localhost:8412" // export const SOIL_BACKEND = DEV_MODE ? "https://iot.wzl-mq.rwth-aachen.de/soil-backend" : "https://iot.wzl-mq.rwth-aachen.de/soil-backend" \ No newline at end of file diff --git a/src/features/demoProjects/DemoProjects.tsx b/src/features/demoProjects/DemoProjects.tsx new file mode 100644 index 0000000000000000000000000000000000000000..8fcea6c3882849d2720678948bab51e3b6917ed1 --- /dev/null +++ b/src/features/demoProjects/DemoProjects.tsx @@ -0,0 +1,88 @@ +import React from 'react'; +import ImageIcon from '@mui/icons-material/Image'; +import IconButton from '@mui/material/IconButton'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import { useAppDispatch } from '../../store/hooks'; +import { addTextModel, importPrivateProject, Project, selectPrivateProjects } from '../soil-editor/soileditorSlice'; +import { Tooltip, Divider, Avatar, ListItemAvatar, ListItem, ListItemText, List} from '@mui/material'; +export function DemoProjects() { + const dispatch = useAppDispatch() + + const addRobotModel = () => { + fetch("/../../soil/robot.soil").then(e => e.text()).then(e => dispatch(addTextModel(["Robot", "Robot", e]))); + } + + const addMonitoringModel = () => { + fetch("/../../soil/monitoring.soil").then(e => e.text()).then(e => dispatch(addTextModel(["Monitoring", "Monitoring", e]))); + } + + const addLaserTrackerModel = () => { + fetch("/../../soil/lasertracker.soil").then(file => file.text()).then(lasertracker => { + fetch("/../../soil/base_stations.soil").then(file2 => file2.text()).then(base_stations => { + fetch("/../../soil/mobile_entities.soil").then(file3 => file3.text()).then(mobile_entities => { + fetch("/../../soil/utils.soil").then(file4 => file4.text()).then(utils => { + dispatch(addTextModel(["Lasertracker", "lasertracker", lasertracker, "mobile_entities", mobile_entities, "base_stations", base_stations, "utils", utils])) + }) + }) + }) + }); + } + return ( + <List sx={{overflowY: "auto", width: '100%', bgcolor: 'background.paper' }}> + <div key={"DemoProjectRobot"}><ListItem secondaryAction={ + <IconButton onClick={addRobotModel} edge="end" aria-label="comments"> + <Tooltip title="Import this Project"> + <div> + <CloudDownloadIcon /> + </div> + </Tooltip> + </IconButton> + }> + <ListItemAvatar> + <Avatar> + <ImageIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={<div>{"Robot"}<small> {"1.0"}</small></div>}/> + </ListItem> + <Divider variant="inset" component="li" /> + </div> + <div key={"DemoProjectMonitoring"}><ListItem secondaryAction={ + <IconButton onClick={addMonitoringModel} edge="end" aria-label="comments"> + <Tooltip title="Import this Project"> + <div> + <CloudDownloadIcon /> + </div> + </Tooltip> + </IconButton> + }> + <ListItemAvatar> + <Avatar> + <ImageIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={<div>{"Monitoring"}<small> {"1.0"}</small></div>}/> + </ListItem> + <Divider variant="inset" component="li" /> + </div> + <div key={"DemoProjectLasertracker"}><ListItem secondaryAction={ + <IconButton onClick={addLaserTrackerModel} edge="end" aria-label="comments"> + <Tooltip title="Import this Project"> + <div> + <CloudDownloadIcon /> + </div> + </Tooltip> + </IconButton> + }> + <ListItemAvatar> + <Avatar> + <ImageIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={<div>{"Lasertracker"}<small> {"1.0"}</small></div>}/> + </ListItem> + <Divider variant="inset" component="li" /> + </div> + </List> + ); +} \ No newline at end of file diff --git a/src/features/privateProjects/PrivateProjects.tsx b/src/features/privateProjects/PrivateProjects.tsx new file mode 100644 index 0000000000000000000000000000000000000000..afe4226932b27de93b6ceee6d2bdffe59c8f73c3 --- /dev/null +++ b/src/features/privateProjects/PrivateProjects.tsx @@ -0,0 +1,70 @@ +import React, { useState } from 'react'; +import ImageIcon from '@mui/icons-material/Image'; +import IconButton from '@mui/material/IconButton'; +import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; +import { useAppSelector, useAppDispatch } from '../../store/hooks'; +import { deleteProjectById, importPrivateProject, Project, selectPrivateProjects } from '../soil-editor/soileditorSlice'; +import { Tooltip, Divider, Avatar, ListItemAvatar, ListItem, ListItemText, List} from '@mui/material'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { DeleteWarningeDialog } from './deleteWarningDialog'; +export function PrivateProjects() { + const privateProjects = useAppSelector(selectPrivateProjects); + const dispatch = useAppDispatch(); + const [selectedPid, setSelectedPid] = useState("" as string); + const [selectedProjectName, setSelectedProjectName] = useState("" as string); + const [open, setOpen] = useState(false); + + const handleDelete = (success: boolean) => { + const pid = selectedPid; + + setOpen(false); + if(success && (selectedPid !== "")) dispatch(deleteProjectById(pid)); + setSelectedPid(""); + setSelectedProjectName(""); + } + + const handleDeleteButtonClick = (pid: string, projectName: string) => { + console.log("que?") + setSelectedPid(pid); + setSelectedProjectName(projectName); + setOpen(true); + } + + return ( + <List sx={{overflowY: "auto", width: '100%', bgcolor: 'background.paper' }}> + { + Object.entries(privateProjects).map(([pid, project]) => { + return <div key={"PrivatecProject" + pid + project.version}><ListItem secondaryAction={ + <div> + <IconButton onClick={() => dispatch(importPrivateProject(project))} edge="end" aria-label="comments"> + <Tooltip title="Import this Project"> + <div> + <CloudDownloadIcon /> + </div> + </Tooltip> + </IconButton> + <IconButton sx={{ml:3}} onClick={() => handleDeleteButtonClick(pid, project.name)} edge="end" aria-label="comments"> + <Tooltip title="Delete this Project"> + <div> + <DeleteIcon color='error' /> + </div> + </Tooltip> + </IconButton> + </div> + }> + <ListItemAvatar> + <Avatar> + <ImageIcon /> + </Avatar> + </ListItemAvatar> + <ListItemText primary={<div>{project.name}<small> {project.version}</small></div>} secondary={pid} /> + + </ListItem> + <Divider variant="inset" component="li" /> + </div> + }) + } + <DeleteWarningeDialog projectName={selectedProjectName} open={open} handleClose={handleDelete}></DeleteWarningeDialog> + </List> + ); +} \ No newline at end of file diff --git a/src/features/privateProjects/deleteWarningDialog.tsx b/src/features/privateProjects/deleteWarningDialog.tsx new file mode 100644 index 0000000000000000000000000000000000000000..435a209c7efbf712a50bc48afe1fd5afa11e3c47 --- /dev/null +++ b/src/features/privateProjects/deleteWarningDialog.tsx @@ -0,0 +1,34 @@ +import { Button } from '@mui/material'; +import Dialog from '@mui/material/Dialog'; +import DialogActions from '@mui/material/DialogActions'; +import DialogContent from '@mui/material/DialogContent'; +import DialogContentText from '@mui/material/DialogContentText'; +import DialogTitle from '@mui/material/DialogTitle'; +import DeleteIcon from '@mui/icons-material/Delete'; + +type Props = { + projectName: string; + open: boolean; + handleClose: (success: boolean) => void; +} + +export const DeleteWarningeDialog = ({projectName, open, handleClose}: Props): JSX.Element => { + return ( + <Dialog + open={open} + onClose={()=>{handleClose(false)}} + > + <DialogTitle>Delete Project {projectName}?</DialogTitle> + <DialogContent> + Are you sure you want to permanently delete {projectName}? Unless the project is currently + open in your local projects, this action is irreversible. + </DialogContent> + <DialogActions> + <Button variant="contained" onClick={()=>{handleClose(false)}} autoFocus> + Keep + </Button> + <Button variant="contained" startIcon={<DeleteIcon />} onClick={()=>{handleClose(true)}}>Delete</Button> + </DialogActions> + </Dialog> + ) +} \ No newline at end of file diff --git a/src/features/publicProjects/PublicProjects.tsx b/src/features/publicProjects/PublicProjects.tsx index df9fc87a700dfa0cfc836f201c365e9c3108bf55..1f3a2ee5128007688575afe25391614c1ea0465b 100644 --- a/src/features/publicProjects/PublicProjects.tsx +++ b/src/features/publicProjects/PublicProjects.tsx @@ -3,7 +3,7 @@ import ImageIcon from '@mui/icons-material/Image'; import IconButton from '@mui/material/IconButton'; import CloudDownloadIcon from '@mui/icons-material/CloudDownload'; import { useAppSelector, useAppDispatch } from '../../store/hooks'; -import { importProject, Project } from '../soil-editor/soileditorSlice'; +import { importPublicProject, Project } from '../soil-editor/soileditorSlice'; import { Tooltip,Divider,Avatar,ListItemAvatar ,ListItem,ListItemText,List} from '@mui/material'; import { DATA_BACKEND } from '../../const'; export function PublicProjects() { @@ -11,12 +11,12 @@ export function PublicProjects() { const [status, setStatus] = React.useState("loading") const dispatch = useAppDispatch() React.useEffect(()=>{ - fetch(DATA_BACKEND + "/projects", { + fetch(DATA_BACKEND + "/publicProjects", { method: 'GET', redirect: 'follow' }) .then(response => response.text()) - .then(result => {setProjects(JSON.parse(result)); setStatus("finished")}) + .then(result => {setProjects(JSON.parse(result)); setStatus("finished"); console.log(JSON.parse(result))}) .catch(error => setStatus("error")); },[]); @@ -38,7 +38,7 @@ if(projects.length < 1) { { projects.map((element : {projectName : string, version : string, id: string, project: Project }) => { return <div key={"PublicProject" + element.id + element.version}><ListItem secondaryAction={ - <IconButton onClick={() => dispatch(importProject(element.project))} edge="end" aria-label="comments"> + <IconButton onClick={() => dispatch(importPublicProject(element.project))} edge="end" aria-label="comments"> <Tooltip title="Import this Project"> <div> <CloudDownloadIcon /> diff --git a/src/features/soil-editor/BottomNavigationDrawer/BottomNavigationButton.tsx b/src/features/soil-editor/BottomNavigationDrawer/BottomNavigationButton.tsx index 48df1d9e2811326f77c649cb5e6fd6e13b664100..24af840c8c46c50de9d760f5efea7c1dbb1d4e62 100644 --- a/src/features/soil-editor/BottomNavigationDrawer/BottomNavigationButton.tsx +++ b/src/features/soil-editor/BottomNavigationDrawer/BottomNavigationButton.tsx @@ -10,7 +10,7 @@ import SpeedDialAction from '@mui/material/SpeedDialAction'; import SpeedDialIcon from '@mui/material/SpeedDialIcon'; import { useParams } from 'react-router-dom'; import { useAppDispatch, useAppSelector } from "../../../store/hooks"; -import { SoilComponent, SoilEnum, SoilFunction, SoilInterface, SoilMeasurement, SoilParameter, selectProjects, setGraphModel } from '../soileditorSlice'; +import { SoilComponent, SoilEnum, SoilFunction, SoilInterface, SoilMeasurement, SoilParameter, SoilVariable, selectProjects, setGraphModel } from '../soileditorSlice'; import { v4 as uuidv4 } from 'uuid'; @@ -92,6 +92,24 @@ export default function BottomNavigationButton({ fileName }: BottomNavigationBut currentGraphModel.push(newComponent) dispatch(setGraphModel([projectId, fileName, currentGraphModel])) } + function addVariable() { + let currentGraphModel = JSON.parse(JSON.stringify(projects[projectId].files[fileName].graphModel)); + let newComponent: SoilVariable = { + cardOpen: false, + elementType: "variable", + name: "New Variable", + description: "A New Variable.", + dimension: [], + datatype: "int", + range: [0, 1], + unit: "CEL", + default: "0", + constant: false, + uuid: uuidv4() + }; + currentGraphModel.push(newComponent) + dispatch(setGraphModel([projectId, fileName, currentGraphModel])) + } function addFunction() { let currentGraphModel = JSON.parse(JSON.stringify(projects[projectId].files[fileName].graphModel)); let newComponent: SoilFunction = { @@ -141,6 +159,7 @@ export default function BottomNavigationButton({ fileName }: BottomNavigationBut { icon: <ExtensionIcon />, name: 'Add Component', onclick: addComponent }, { icon: <Circle />, name: 'Add Measurement', onclick: addMeasurement }, { icon: <EditIcon />, name: 'Add Parameter', onclick: addParameter }, + { icon: <EditIcon />, name: 'Add Variable', onclick: addVariable }, { icon: <SettingsIcon />, name: 'Add Function', onclick: addFunction }, { icon: <ListIcon />, name: 'Add Enum', onclick: addEnum }, { icon: <RadioButtonCheckedIcon />, name: 'Add Interface', onclick: addInterface }, diff --git a/src/features/soil-editor/SoilCardList/SoilCardList.tsx b/src/features/soil-editor/SoilCardList/SoilCardList.tsx index 70de13cd5fc60848d97796a1531b922b270ae3be..2f8c8dc1f9cfa465acef0f3fc402d1b2bd48be2b 100644 --- a/src/features/soil-editor/SoilCardList/SoilCardList.tsx +++ b/src/features/soil-editor/SoilCardList/SoilCardList.tsx @@ -6,8 +6,7 @@ import { ComponentCard } from './SoilCards/ComponentCard'; import { EnumCard } from './SoilCards/EnumCard'; import { FunctionCard } from './SoilCards/FunctionCard'; import { InterfaceCard } from './SoilCards/InterfaceCard'; -import { MeasurementCard } from './SoilCards/MeasurementCard'; -import { ParameterCard } from './SoilCards/ParameterCard'; +import { VariableCard } from './SoilCards/VariableCard'; type SoilCardListProps = { fileName: string } @@ -55,10 +54,8 @@ export function SoilCardList({ fileName }: SoilCardListProps) { return <ComponentCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index}></ComponentCard> case "function": return <FunctionCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index}></FunctionCard> - case "parameter": - return <ParameterCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index}></ParameterCard> - case "measurement": - return <MeasurementCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index}></MeasurementCard> + case "variable": + return <VariableCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index}></VariableCard> case "enum": return <EnumCard handleOpen={handleOpen} cardOpen={cardsOpen[index]} projectId={projectId} fileName={fileName} elementIndex={index} /> case "interface": diff --git a/src/features/soil-editor/SoilCardList/SoilCards/ComponentCard.tsx b/src/features/soil-editor/SoilCardList/SoilCards/ComponentCard.tsx index 61fb879f3fae01d4a4399191925c17dda983b82a..8a4448fe99db6b1178fa91f511f4478631adc8c5 100644 --- a/src/features/soil-editor/SoilCardList/SoilCards/ComponentCard.tsx +++ b/src/features/soil-editor/SoilCardList/SoilCards/ComponentCard.tsx @@ -10,7 +10,7 @@ import DynamicFeedIcon from '@mui/icons-material/DynamicFeed'; import { Card, CardActions, CardContent, CardHeader, Collapse, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField, Tooltip } from '@mui/material'; import React, { useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from "../../../../store/hooks"; -import { NamedElement, SoilComponent, SoilEnum, SoilEvent, SoilFunction, SoilInterface, SoilMeasurement, SoilParameter, SoilStream, addComponentToFile, addFunctionToFile, addMeasurementToFile, addParameterToFile, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; +import { NamedElement, SoilComponent, SoilEnum, SoilEvent, SoilFunction, SoilInterface, SoilMeasurement, SoilParameter, SoilStream, SoilVariable, addComponentToFile, addFunctionToFile, addVariableToFile, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; import DropdownButton from './DropdownButton'; import { EventInputList } from './EventInputList'; import EditModal from './InstancedChildEditModal'; @@ -111,7 +111,18 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han </TableCell> <TableCell > <Stack direction="row"> - {componentRef.current.measurements[i]?.name} + <div style = {{ display: thisComponent.measurements[i]?.internal ? "" : "none", whiteSpace: "pre-wrap", color: "#00549F"}}>internal </div> + {thisComponent.measurements[i]?.customName !== undefined && + <i style = {{ color: "orange"}}>{componentRef.current.measurements[i]?.customName}</i> + } + {thisComponent.measurements[i]?.customName !== undefined && + <div>(</div> + } + {thisComponent.measurements[i]?.typeName} + {thisComponent.measurements[i]?.customName !== undefined && + <div>) </div> + } + <div style={{whiteSpace: "pre-wrap"}}> {thisComponent.measurements[i]?.name}</div> <div style={{ display: thisComponent.measurements[i] ? "" : "none" }}> <EditModal childName={thisComponent.measurements[i]?.name} childIndex={i} handleChange={handleInstanceUpdate} buttonText='measurements' /> </div> @@ -120,10 +131,20 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han </TableCell> <TableCell > <Stack direction="row"> - {componentRef.current.parameters[i]?.name} - <Tooltip title="Constant Parameter" style={{ display: thisComponent.parameters[i] && componentRef.current.parameters[i].constant ? "inline" : "none" }}> - <DoNotDisturbIcon /> - </Tooltip> + <div style = {{ display: thisComponent.parameters[i]?.constant ? "" : "none", whiteSpace: "pre-wrap", color: "#00549F"}}>constant </div> + {thisComponent.parameters[i]?.customName !== undefined && + <i style = {{ color: "orange"}}>{componentRef.current.parameters[i]?.customName}</i> + } + {thisComponent.parameters[i]?.customName !== undefined && + <div>(</div> + } + {thisComponent.parameters[i]?.typeName} + {thisComponent.parameters[i]?.customName !== undefined && + <div>) </div> + } + <div style={{whiteSpace: "pre-wrap"}}> {thisComponent.parameters[i]?.name}</div> + {thisComponent.parameters[i]?.initialValue !== "" && thisComponent.parameters[i]?.initialValue !== undefined && + <div style={{whiteSpace: "pre-wrap"}}> = {thisComponent.parameters[i]?.initialValue}</div>} <div style={{ display: thisComponent.parameters[i] ? "" : "none" }}> <EditModal shouldRenderCheckbox={true} propConstantChecked={thisComponent.parameters[i]?.constant} childName={thisComponent.parameters[i]?.name} childIndex={i} handleChange={handleInstanceUpdate} buttonText='parameters' /> </div> @@ -138,7 +159,7 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han } function generateValidComponentMenuItems() { - let validCompMenuItem: { itemText: string, onClick: Function }[] = [] + let validCompMenuItem: { itemCard?: SoilComponent, itemText: string, onClick: Function }[] = [] Object.entries(projects[projectId].files).forEach(([key, value]) => { //Return if file is not the current file and not included in the import list @@ -149,12 +170,18 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han Object.entries(value.graphModel).forEach(([secKey, secValue]) => { if (secValue.uuid !== thisComponent.uuid && secValue.elementType === "component" && thisComponent.components.filter(x => x.value === secValue.uuid).length === 0) { validCompMenuItem.push({ - itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, dynamicChecked: boolean) => { + itemCard: secValue, + itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, dynamicChecked: boolean, description?: string, customName?: string, overrides?: Map<string, string>) => { let validComponentInstance: NamedElement<string> = { name: instancedName, + typeName: secValue.name, value: secValue.uuid, - dynamic: dynamicChecked + dynamic: dynamicChecked, + description: description, + customName: customName, + overrides: overrides }; + console.log(validComponentInstance); componentRef.current.components.push(validComponentInstance); dispatch(setGraphModelCard([projectId, fileName, elementIndex, componentRef.current])) @@ -172,7 +199,7 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han } function generateValidFunctionMenuItems() { - let validFuncMenuItem: { itemText: string, onClick: Function }[] = [] + let validFuncMenuItem: { itemCard?: SoilFunction, itemText: string, onClick: Function }[] = [] Object.entries(projects[projectId].files).forEach(([key, value]) => { //Return if file is not the current file and not included in the import list @@ -183,10 +210,15 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han Object.entries(value.graphModel).forEach(([secKey, secValue]) => { if (secValue.elementType === "function" && thisComponent.functions.filter(x => x.value === secValue.uuid).length === 0) { validFuncMenuItem.push({ - itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string) => { + itemCard: secValue, + itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, streaming: boolean, description?: string, customName?: string) => { let validComponentInstance: NamedElement<string> = { name: instancedName, + typeName: secValue.name, value: secValue.uuid, + streaming: streaming, + description: description, + customName: customName }; componentRef.current.functions.push(validComponentInstance); dispatch(setGraphModelCard([projectId, fileName, elementIndex, componentRef.current])) @@ -204,7 +236,7 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han } function generateValidParameterMenuItems() { - let validParaMenuItem: { itemText: string, onClick: Function }[] = [] + let validParaMenuItem: { itemCard?: SoilVariable, itemText: string, onClick: Function }[] = [] Object.entries(projects[projectId].files).forEach(([key, value]) => { //Return if file is not the current file and not included in the import list @@ -213,31 +245,37 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han } Object.entries(value.graphModel).forEach(([secKey, secValue]) => { - if (secValue.elementType === "parameter" && thisComponent.parameters.filter(x => x.value === secValue.uuid).length === 0) { + if (secValue.elementType === "variable") { validParaMenuItem.push({ - itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, constant: boolean) => { + itemCard: secValue, + itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, constant: boolean, initialValue: string | number | boolean, description?: string, customName?: string) => { let validComponentInstance: NamedElement<string> = { name: instancedName, + typeName: secValue.name, value: secValue.uuid, - constant: constant + constant: constant, + initialValue: initialValue, + description: description, + customName: customName }; componentRef.current.parameters.push(validComponentInstance); + console.log(componentRef.current); dispatch(setGraphModelCard([projectId, fileName, elementIndex, componentRef.current])) } }) } }) - }) + }); validParaMenuItem.push({ itemText: "Add New +", onClick: () => { - dispatch(addParameterToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } - }) + }); return validParaMenuItem } function generateValidMeasurementMenuItems() { - let validMesMenuItem: { itemText: string, onClick: Function }[] = [] + let validMesMenuItem: { itemCard?: SoilVariable, itemText: string, onClick: Function }[] = [] Object.entries(projects[projectId].files).forEach(([key, value]) => { //Return if file is not the current file and not included in the import list @@ -246,13 +284,17 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han } Object.entries(value.graphModel).forEach(([secKey, secValue]) => { - if (secValue.elementType === "measurement" && thisComponent.measurements.filter(x => x.value === secValue.uuid).length === 0) { + if ((secValue.elementType === "variable")) { validMesMenuItem.push({ - itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, constant: boolean) => { + itemCard: secValue, + itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string, constant: boolean, description?: string, customName?: string) => { let validComponentInstance: NamedElement<string> = { name: instancedName, + typeName: secValue.name, value: secValue.uuid, - constant: constant + internal: constant, + description: description, + customName: customName }; componentRef.current.measurements.push(validComponentInstance); dispatch(setGraphModelCard([projectId, fileName, elementIndex, componentRef.current])) @@ -263,14 +305,14 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han }) validMesMenuItem.push({ itemText: "Add New +", onClick: () => { - dispatch(addMeasurementToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } - }) + }); return validMesMenuItem } const getCardFromUUID = (uuid: string) => { - var outputValue: (SoilComponent | SoilParameter | SoilMeasurement | SoilFunction | SoilInterface | SoilEnum) = projects[projectId].files[fileName].graphModel[0]; + var outputValue: (SoilComponent | SoilParameter | SoilMeasurement | SoilVariable | SoilFunction | SoilInterface | SoilEnum) = projects[projectId].files[fileName].graphModel[0]; Object.entries(projects[projectId].files).forEach(([key, value]) => { //Return if file is not the current file and not included in the import list @@ -350,10 +392,10 @@ export function ComponentCard({ projectId, fileName, elementIndex, cardOpen, han </CardContent> <CardActions> <Stack direction={"row"} spacing={1}> - <DropdownButton shouldRenderDynamicCheckbox={true} startIcon={<ExtensionIcon />} buttonText="Add Component" menuItems={generateValidComponentMenuItems()} ></DropdownButton> - <DropdownButton startIcon={<SettingsIcon />} buttonText="Add Function" menuItems={generateValidFunctionMenuItems()} ></DropdownButton> - <DropdownButton startIcon={<Circle />} buttonText="Add Measurement" menuItems={generateValidMeasurementMenuItems()} ></DropdownButton> - <DropdownButton shouldRenderCheckbox={true} startIcon={<EditIcon />} buttonText="Add Parameter" menuItems={generateValidParameterMenuItems()} ></DropdownButton> + <DropdownButton type={"component"} shouldRenderDynamicCheckbox={true} startIcon={<ExtensionIcon />} buttonText="Add Component" menuItems={generateValidComponentMenuItems()} ></DropdownButton> + <DropdownButton type={"function"} shouldRenderStreamingCheckbox={true} startIcon={<SettingsIcon />} buttonText="Add Function" menuItems={generateValidFunctionMenuItems()} ></DropdownButton> + <DropdownButton type={"measurement"} shouldRenderInternalCheckbox={true} startIcon={<Circle />} buttonText="Add Measurement" menuItems={generateValidMeasurementMenuItems()} ></DropdownButton> + <DropdownButton type={"parameter"} shouldRenderConstantCheckbox={true} shouldRenderValueTextbox={true} startIcon={<EditIcon />} buttonText="Add Parameter" menuItems={generateValidParameterMenuItems()} ></DropdownButton> </Stack> </CardActions> </Collapse> diff --git a/src/features/soil-editor/SoilCardList/SoilCards/DropdownButton.tsx b/src/features/soil-editor/SoilCardList/SoilCards/DropdownButton.tsx index fb86d89d3194cf96288bf5c8db23af3368cc1d47..667f3c748f432fe3595ab8351bd8d9b32ec219a4 100644 --- a/src/features/soil-editor/SoilCardList/SoilCards/DropdownButton.tsx +++ b/src/features/soil-editor/SoilCardList/SoilCards/DropdownButton.tsx @@ -1,20 +1,43 @@ -import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Menu, MenuItem, Stack, TextField } from '@mui/material'; +import { Button, Checkbox, Dialog, DialogActions, DialogContent, DialogTitle, FormControlLabel, Menu, MenuItem, Stack, Switch, TextField, Typography } from '@mui/material'; import React, { useState } from 'react'; +import { useAppSelector } from '../../../../store/hooks'; +import { NamedElement, SoilComponent, SoilFunction, SoilMeasurement, SoilParameter, SoilVariable } from '../../soileditorSlice'; +import { RangeValue } from './RangeInput'; +import { VariableValueInput } from './VariableValueInput'; interface DropdownButtonProps { buttonText: string; - menuItems: { itemText: string, onClick: Function }[]; + type: "measurement" | "parameter" | "component" | "function" | "function_arguments" | "function_returns"; + menuItems: { itemText: string, onClick: Function, itemCard?: SoilComponent | SoilVariable | SoilFunction | SoilMeasurement | SoilParameter }[]; startIcon: any, - shouldRenderCheckbox?: boolean, + shouldRenderConstantCheckbox?: boolean, + shouldRenderInternalCheckbox?: boolean, + shouldRenderStreamingCheckbox?: boolean, shouldRenderDynamicCheckbox?: boolean, + shouldRenderValueTextbox?: boolean, } -const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, startIcon, shouldRenderCheckbox = false, shouldRenderDynamicCheckbox = false }) => { +const DropdownButton: React.FC<DropdownButtonProps> = ({ type, buttonText, menuItems, startIcon, shouldRenderStreamingCheckbox = false, shouldRenderConstantCheckbox = false, shouldRenderInternalCheckbox = false, shouldRenderDynamicCheckbox = false, shouldRenderValueTextbox = false }) => { const [anchorEl, setAnchorEl] = useState<null | HTMLElement>(null); const [dialogOpen, setDialogOpen] = useState(false); const [dialogContent, setDialogContent] = useState<{ itemText: string, onClick: Function }>(); const [instanceName, setInstanceName] = useState(""); + const [initialValue, setInitialValue] = useState("" as string | number | boolean); + const [overrideEnabled, setOverrideEnabled] = useState(false); const [constantChecked, setConstantChecked] = useState(false); + const [internalChecked, setInternalChecked] = useState(false); + const [streamingChecked, setStreamingChecked] = useState(false); const [dynamicChecked, setDynamicChecked] = useState(false); + const [description, setDescription] = useState(""); + const [customName, setCustomName] = useState(""); + const [itemDatatype, setItemDatatype] = useState(""); + const [itemRange, setItemRange] = useState(undefined as RangeValue); + const [incorrectInputError, setIncorrectInputError] = useState(false); + const [overrides, setOverrides] = useState(new Map()); + const [itemDefinition, setItemDefinition] = useState(undefined as unknown as SoilComponent | SoilVariable | SoilFunction | SoilMeasurement | SoilParameter); + + const toggleOverride = () => { + setOverrideEnabled(!overrideEnabled); + } const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { setAnchorEl(event.currentTarget); @@ -28,14 +51,60 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, setConstantChecked(event.target.checked); } + const handleInternalCheckbox = (event: React.ChangeEvent<HTMLInputElement>) => { + setInternalChecked(event.target.checked); + } + + const handleStreamingCheckbox = (event: React.ChangeEvent<HTMLInputElement>) => { + setStreamingChecked(event.target.checked); + } + const handleText = (e: any) => { setInstanceName(e.target.value) } - const handleMenuItemClick = (item: { itemText: string, onClick: Function }) => { + const handleDescriptionChange = (e: any) => { + setDescription(e.target.value); + } + + const handleNameChange = (e: any) => { + setCustomName(e.target.value); + } + + const handleInitialValueChange = (value: string | number | boolean, error: boolean) => { + if (error) { + setIncorrectInputError(true); + } + else { + setInitialValue(value) + setIncorrectInputError(false); + } + } + + const handleOverride = (e: any) => { + console.log(e); + if (e.target.value === e.target.defaultValue) { + let new_map = new Map(overrides); + new_map.delete(e.target.id); + setOverrides(new_map); + } + else { + setOverrides(new Map(overrides).set(e.target.id, e.target.value)); + } + console.log(overrides); + } + + const handleMenuItemClick = (item: { itemText: string, onClick: Function, itemCard?: SoilComponent | SoilVariable | SoilFunction | SoilMeasurement | SoilParameter }) => { setDialogContent(item); if (item.itemText !== "Add New +") { setDialogOpen(true); + if (item.itemCard !== undefined) { + setItemDefinition(item.itemCard); + setDescription(item.itemCard.description); + setCustomName(item.itemCard.name); + if('datatype' in item.itemCard) setItemDatatype(item.itemCard.datatype); + if('range' in item.itemCard) setItemRange(item.itemCard.range); + } } else { item.onClick() @@ -49,25 +118,51 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, // ... // Close the dialog - if (shouldRenderCheckbox) { - dialogContent?.onClick(instanceName, constantChecked) - setDialogOpen(false); - } - else if (shouldRenderDynamicCheckbox) { - dialogContent?.onClick(instanceName, dynamicChecked) - setDialogOpen(false) - } - else { - dialogContent?.onClick(instanceName) - setDialogOpen(false); + const f_description = description !== itemDefinition.description ? description : undefined; + const f_name = customName !== itemDefinition.name ? customName : undefined; + switch(type) { + case "parameter": { + dialogContent?.onClick(instanceName, constantChecked, initialValue, f_description, f_name); + setDialogOpen(false); + break; + } + case "measurement": { + dialogContent?.onClick(instanceName, internalChecked, f_description, f_name); + setDialogOpen(false); + break; + } + case "component": { + dialogContent?.onClick(instanceName, dynamicChecked, f_description, f_name, overrides); + setDialogOpen(false); + break; + } + case "function": { + dialogContent?.onClick(instanceName, f_description, f_name); + setDialogOpen(false); + break; + } } - } const handleDialogAbort = () => { // Close the dialog without performing any action setDialogOpen(false); } + + function generateParameterOverrideFields(parameters: NamedElement<string>[]): React.ReactNode { + return( + <Stack> + {parameters && parameters.map((val) => ( + val.initialValue ? ( + <TextField label={val.name} id={val.name} defaultValue={val.initialValue} sx={{mt: 1}} onChange={handleOverride}></TextField> + ) : ( + <TextField label={val.name}></TextField> + ) + ))} + </Stack> + ) + } + return ( <div> <Button startIcon={startIcon} aria-controls="simple-menu" aria-haspopup="true" onClick={handleClick}> @@ -87,13 +182,20 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, ))} </Menu> <Dialog open={dialogOpen} onClose={handleDialogAbort}> - <DialogTitle>Confirmation</DialogTitle> + <DialogTitle>Confirmation + {type==="component" && ( + <FormControlLabel control={<Switch sx={{ml: 30}}/>} label="Override?" onChange={toggleOverride}/> + )} + </DialogTitle> <DialogContent> <Stack flexDirection="column" > - - <TextField onChange={handleText} style={{ marginTop: "0.5rem" }} label="Instance Name"> - </TextField> - {shouldRenderCheckbox && ( + <TextField onChange={handleText} style={{ marginTop: "0.5rem" }} label="Instance Name"/> + <TextField onChange={handleNameChange} style={{ marginTop: "1.5rem" }} label="Name" value={customName}/> + <TextField onChange={handleDescriptionChange} style={{ marginTop: "1.5rem" }} label="Description" value={description}/> + {shouldRenderValueTextbox && ( + <VariableValueInput datatype={itemDatatype} range={itemRange} onChange={handleInitialValueChange}/> + )} + {shouldRenderConstantCheckbox && ( <FormControlLabel control={ <Checkbox @@ -105,7 +207,30 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, label="Constant" /> )} - + {shouldRenderInternalCheckbox && ( + <FormControlLabel + control={ + <Checkbox + checked={internalChecked} + onChange={handleInternalCheckbox} + inputProps={{ 'aria-label': 'Internal' }} + /> + } + label="Internal" + /> + )} + {shouldRenderStreamingCheckbox && ( + <FormControlLabel + control={ + <Checkbox + checked={streamingChecked} + onChange={handleStreamingCheckbox} + inputProps={{ 'aria-label': 'Streaming' }} + /> + } + label="Streaming" + /> + )} {shouldRenderDynamicCheckbox && ( <FormControlLabel control={ @@ -118,12 +243,12 @@ const DropdownButton: React.FC<DropdownButtonProps> = ({ buttonText, menuItems, label="Dynamic" /> )} + {overrideEnabled && generateParameterOverrideFields(itemDefinition.parameters)} </Stack> - </DialogContent> <DialogActions> <Button onClick={handleDialogAbort}>Abort</Button> - <Button disabled={instanceName === ""} onClick={handleDialogConfirm}>Confirm</Button> + <Button disabled={instanceName === "" || incorrectInputError} onClick={handleDialogConfirm}>Confirm</Button> </DialogActions> </Dialog> </div> diff --git a/src/features/soil-editor/SoilCardList/SoilCards/FunctionCard.tsx b/src/features/soil-editor/SoilCardList/SoilCards/FunctionCard.tsx index 13d09f3ad7dccb0d1d47bd9d4323e3e3f691dfa2..6647632aec717999378e85681568476ca62ee1c9 100644 --- a/src/features/soil-editor/SoilCardList/SoilCards/FunctionCard.tsx +++ b/src/features/soil-editor/SoilCardList/SoilCards/FunctionCard.tsx @@ -8,7 +8,7 @@ import TrendingFlatIcon from '@mui/icons-material/TrendingFlat'; import { Card, CardActions, CardContent, CardHeader, Collapse, Paper, Stack, Table, TableBody, TableCell, TableContainer, TableHead, TableRow, TextField } from '@mui/material'; import React, { useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from "../../../../store/hooks"; -import { NamedElement, SoilFunction, SoilMeasurement, SoilParameter, addMeasurementToFile, addParameterToFile, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; +import { NamedElement, SoilFunction, SoilMeasurement, SoilParameter, SoilVariable, addVariableToFile, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; import DropdownButton from './DropdownButton'; import EditModal from './InstancedChildEditModal'; @@ -61,11 +61,12 @@ export function FunctionCard({ projectId, fileName, elementIndex, cardOpen, hand } Object.entries(value.graphModel).forEach(([secKey, secValue]) => { - if ((secValue.elementType === "measurement" || secValue.elementType === "parameter") && thisFunction.arguments.filter(x => x.name === secValue.name).length === 0) { + if ((secValue.elementType === "variable") && thisFunction.arguments.filter(x => x.name === secValue.name).length === 0) { validArgMenuItem.push({ itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string) => { - let validComponentInstance: NamedElement<SoilParameter | SoilMeasurement> = { + let validComponentInstance: NamedElement<SoilVariable> = { name: instancedName, + typeName: secValue.name, value: secValue, }; functionRef.current.arguments.push(validComponentInstance); @@ -77,12 +78,12 @@ export function FunctionCard({ projectId, fileName, elementIndex, cardOpen, hand }) validArgMenuItem.push({ itemText: "Add New Measurement +", onClick: () => { - dispatch(addMeasurementToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } }) validArgMenuItem.push({ itemText: "Add New Parameter +", onClick: () => { - dispatch(addParameterToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } }) return validArgMenuItem @@ -98,11 +99,12 @@ export function FunctionCard({ projectId, fileName, elementIndex, cardOpen, hand } Object.entries(value.graphModel).forEach(([secKey, secValue]) => { - if ((secValue.elementType === "measurement" || secValue.elementType === "parameter") && thisFunction.returns.filter(x => x.name === secValue.name).length === 0) { + if ((secValue.elementType === "variable") && thisFunction.returns.filter(x => x.name === secValue.name).length === 0) { validCompRetItem.push({ itemText: value.fileName + "." + secValue.name, onClick: (instancedName: string) => { - let validComponentInstance: NamedElement<SoilParameter | SoilMeasurement> = { + let validComponentInstance: NamedElement<SoilVariable> = { name: instancedName, + typeName: secValue.name, value: secValue, }; functionRef.current.returns.push(validComponentInstance); @@ -114,12 +116,12 @@ export function FunctionCard({ projectId, fileName, elementIndex, cardOpen, hand }) validCompRetItem.push({ itemText: "Add New Measurement +", onClick: () => { - dispatch(addMeasurementToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } }) validCompRetItem.push({ itemText: "Add New Parameter +", onClick: () => { - dispatch(addParameterToFile([projectId, fileName])) + dispatch(addVariableToFile([projectId, fileName])) } }) return validCompRetItem @@ -203,8 +205,8 @@ export function FunctionCard({ projectId, fileName, elementIndex, cardOpen, hand </Stack> </CardContent> <CardActions> - <DropdownButton startIcon={<InputIcon />} buttonText="Add Argument" menuItems={generateValidArgumentMenuItems()} ></DropdownButton> - <DropdownButton startIcon={<OutputIcon />} buttonText="Add Return" menuItems={generateValidReturnMenuItems()} ></DropdownButton> + <DropdownButton type={'function_arguments'} startIcon={<InputIcon />} buttonText="Add Argument" menuItems={generateValidArgumentMenuItems()} ></DropdownButton> + <DropdownButton type={"function_returns"} startIcon={<OutputIcon />} buttonText="Add Return" menuItems={generateValidReturnMenuItems()} ></DropdownButton> </CardActions> </Collapse> diff --git a/src/features/soil-editor/SoilCardList/SoilCards/MeasurementCard.tsx b/src/features/soil-editor/SoilCardList/SoilCards/MeasurementCard.tsx deleted file mode 100644 index fba1bca1c355458775bad6f29dda06a39ee95f73..0000000000000000000000000000000000000000 --- a/src/features/soil-editor/SoilCardList/SoilCards/MeasurementCard.tsx +++ /dev/null @@ -1,160 +0,0 @@ -import Circle from '@mui/icons-material/Circle'; -import DeleteIcon from '@mui/icons-material/Delete'; -import ExpandLess from '@mui/icons-material/ExpandLess'; -import ExpandMore from '@mui/icons-material/ExpandMore'; -import { Card, CardContent, CardHeader, Collapse, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Stack, TextField } from '@mui/material'; -import React, { useRef, useState } from 'react'; -import { useAppDispatch, useAppSelector } from "../../../../store/hooks"; -import { SoilEnum, SoilMeasurement, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; -import DimensionInput from './DimensionInput'; -import InputRange, { RangeValue } from './RangeInput'; -type MeasurementCardProps = { - projectId: string, - fileName: string, - elementIndex: number, - cardOpen: boolean, - handleOpen: Function -} - -export function MeasurementCard({ projectId, fileName, elementIndex, cardOpen, handleOpen }: MeasurementCardProps) { - - const dispatch = useAppDispatch(); - const projects = useAppSelector(selectProjects); - const thisMeasurement = projects[projectId].files[fileName].graphModel[elementIndex] as SoilMeasurement - const measurementRef = useRef(JSON.parse(JSON.stringify(thisMeasurement))) - const [update, setUpdate] = useState(false) - const [dimension, setDimension] = useState(measurementRef.current.dimension) - const imports = projects[projectId].files[fileName].imports - - const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { - measurementRef.current[e.target.id] = e.target.value; - setUpdate(!update) - } - - const handleDimensionChange = (newNumbers: number[]) => { - measurementRef.current.dimension = newNumbers - dispatch(setGraphModelCard([projectId, fileName, elementIndex, measurementRef.current])) - setDimension(newNumbers) - } - - const handleRangeChange = (rangeValue: RangeValue) => { - measurementRef.current.range = rangeValue; - setUpdate(!update); - } - - const handleDatatypeChange = (event: SelectChangeEvent<any>) => { - const newDatatype = event.target.value; - measurementRef.current.datatype = newDatatype; - switch (newDatatype) { - case "int": - case "float": - measurementRef.current.range = [undefined, undefined]; - break; - case "enum": - const validEnums = generateValidEnums(); - measurementRef.current.range = [validEnums![0], validEnums![0]] - break; - case 'string': - default: - measurementRef.current.range = undefined; - break; - } - setUpdate(!update) - } - - - - React.useEffect(() => { - return () => { - if (JSON.stringify(measurementRef.current) !== JSON.stringify(thisMeasurement)) { - dispatch(setGraphModelCard([projectId, fileName, elementIndex, measurementRef.current])) - } - - // - } - }, []) - - function generateValidEnums() { - let validEnums: SoilEnum[] = [] - Object.entries(projects[projectId].files).forEach(([key, value]) => { - - //Return if file is not the current file and not included in the import list - if (value.fileName !== fileName && !imports.includes(value.fileName)) { - return - } - - Object.entries(value.graphModel).forEach(([secKey, secValue]) => { - if (secValue.elementType === "enum") { - validEnums.push(secValue) - } - }) - }) - return validEnums - } - - return ( - <Card variant="outlined" sx={{ minWidth: 275 }}> - <Stack direction="row" justifyContent="flex-start" alignItems="center"> - <CardHeader avatar={<Circle />} title={measurementRef.current.name}></CardHeader> - <Stack direction="row" justifyContent="flex-start" alignItems="center" style={{ marginLeft: "auto", marginRight: "1rem" }}> - <DeleteIcon onClick={() => dispatch(deleteSoilElementFromGraph([projectId, fileName, elementIndex]))} /> - <div onClick={() => handleOpen(elementIndex)} style={{ marginTop: "5px" }}> - {cardOpen ? <ExpandLess /> : <ExpandMore />} - </div> - </Stack> - - </Stack> - - <Collapse in={cardOpen} timeout="auto" unmountOnExit> - - <CardContent> - <Stack direction="column" justifyContent="flex-start" alignItems="flex-start" spacing={2}> - <TextField - id="name" - label="Name" - onChange={handleChange} - value={measurementRef.current.name} - onBlur={() => dispatch(setGraphModelCard([projectId, fileName, elementIndex, measurementRef.current]))} - /> - <TextField - style={{ width: "100%" }} - id="description" - label="Description" - multiline - onChange={handleChange} - value={measurementRef.current.description} - /> - <FormControl fullWidth> - <InputLabel>Datatype</InputLabel> - <Select - label="Datatype" - value={measurementRef.current.datatype} - onChange={handleDatatypeChange}> - <MenuItem value="boolean">Boolean</MenuItem> - <MenuItem value="int">Integer</MenuItem> - <MenuItem value="float">Float</MenuItem> - <MenuItem value="string">String</MenuItem> - <MenuItem value="enum">Enum</MenuItem> - <MenuItem value="time">Time</MenuItem> - </Select> - </FormControl> - - - <TextField - style={{ display: ("stringenumbooleantime".includes(measurementRef.current.datatype)) ? "none" : "block" }} - id="unit" - label="Unit" - onChange={handleChange} - value={measurementRef.current.unit} - /> - <DimensionInput numbers={dimension} onChange={(newNumbers: number[]) => handleDimensionChange(newNumbers)} /> - <InputRange projectId={projectId} fileName={fileName} enumValues={measurementRef.current.datatype === "enum" ? generateValidEnums() : []} label="Range" rangeValue={measurementRef.current.range} datatype={measurementRef.current.datatype} onChange={handleRangeChange} /> - </Stack> - </CardContent> - - </Collapse> - - </Card> - - ); -} diff --git a/src/features/soil-editor/SoilCardList/SoilCards/ParameterCard.tsx b/src/features/soil-editor/SoilCardList/SoilCards/VariableCard.tsx similarity index 69% rename from src/features/soil-editor/SoilCardList/SoilCards/ParameterCard.tsx rename to src/features/soil-editor/SoilCardList/SoilCards/VariableCard.tsx index 2dd187f1122d631ee18beefab2f2bf587fed3827..7ef8ce8ee8909f1e368d522d0ff9933e3b23bf94 100644 --- a/src/features/soil-editor/SoilCardList/SoilCards/ParameterCard.tsx +++ b/src/features/soil-editor/SoilCardList/SoilCards/VariableCard.tsx @@ -5,12 +5,12 @@ import ExpandMore from '@mui/icons-material/ExpandMore'; import { Card, CardContent, CardHeader, Collapse, FormControl, InputLabel, MenuItem, Select, SelectChangeEvent, Stack, TextField } from '@mui/material'; import React, { useRef, useState } from 'react'; import { useAppDispatch, useAppSelector } from "../../../../store/hooks"; -import { SoilEnum, SoilParameter, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; +import { SoilEnum, SoilVariable, deleteSoilElementFromGraph, selectProjects, setGraphModelCard } from '../../soileditorSlice'; import DimensionInput from './DimensionInput'; import InputRange, { RangeValue } from './RangeInput'; import { useSelector } from 'react-redux'; import { RootState } from '../../../../store/store'; -type ParameterCardProps = { +type VariableCardProps = { projectId: string, fileName: string, elementIndex: number, @@ -19,51 +19,52 @@ type ParameterCardProps = { } -export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, handleOpen }: ParameterCardProps) { +export function VariableCard({ projectId, fileName, elementIndex, cardOpen, handleOpen }: VariableCardProps) { const dispatch = useAppDispatch(); const project = useSelector((state: RootState) => state.soileditor.projects[projectId]); - const thisParameter = useSelector((state: RootState) => state.soileditor.projects[projectId].files[fileName].graphModel[elementIndex]) as SoilParameter; - // const thisParameter = projects[projectId].files[fileName].graphModel[elementIndex] as SoilParameter + const thisVariable = useSelector((state: RootState) => state.soileditor.projects[projectId].files[fileName].graphModel[elementIndex]) as SoilVariable; + // const thisVariable = projects[projectId].files[fileName].graphModel[elementIndex] as SoilVariable const imports = useSelector((state: RootState) => state.soileditor.projects[projectId].files[fileName].imports); - const parameterRef = useRef(JSON.parse(JSON.stringify(thisParameter))) + const variableRef = useRef(JSON.parse(JSON.stringify(thisVariable))) const [update, setUpdate] = useState(false) - const [dimension, setDimension] = useState(parameterRef.current.dimension) + const [dimension, setDimension] = useState(variableRef.current.dimension) /** * Handles changes of the name and description input field of the parameter * @param e change event */ const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => { - parameterRef.current[e.target.id] = e.target.value; + variableRef.current[e.target.id] = e.target.value; setUpdate(!update) } const handleRangeChange = (rangeValue: RangeValue) => { - parameterRef.current.range = rangeValue; + variableRef.current.range = rangeValue; setUpdate(!update); } const handleDimensionChange = (newNumbers: number[]) => { - parameterRef.current.dimension = newNumbers + console.log(newNumbers); + variableRef.current.dimension = newNumbers setDimension(newNumbers) } const handleDatatypeChange = (event: SelectChangeEvent<any>) => { const newDatatype = event.target.value; - parameterRef.current.datatype = newDatatype; + variableRef.current.datatype = newDatatype; + console.log(variableRef); switch (newDatatype) { case "int": case "float": - parameterRef.current.range = [undefined, undefined]; + variableRef.current.range = [undefined, undefined]; break; case "enum": const validEnums = generateValidEnums(); - parameterRef.current.range = [validEnums![0], validEnums![0]] + variableRef.current.range = [validEnums![0], validEnums![0]] break; case 'string': default: - parameterRef.current.range = undefined; - parameterRef.current.default = null; + variableRef.current.range = undefined; break; } setUpdate(!update) @@ -71,8 +72,8 @@ export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, han React.useEffect(() => { return () => { - if (JSON.stringify(parameterRef.current) !== JSON.stringify(thisParameter)) { - dispatch(setGraphModelCard([projectId, fileName, elementIndex, parameterRef.current])) + if (JSON.stringify(variableRef.current) !== JSON.stringify(thisVariable)) { + dispatch(setGraphModelCard([projectId, fileName, elementIndex, variableRef.current])) } } }, []) @@ -98,7 +99,7 @@ export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, han return ( <Card variant="outlined" sx={{ minWidth: 275 }}> <Stack direction="row" justifyContent="flex-start" alignItems="center" > - <CardHeader avatar={<EditIcon />} title={thisParameter.name}></CardHeader> + <CardHeader avatar={<EditIcon />} title={thisVariable.name}></CardHeader> <Stack direction="row" justifyContent="flex-start" alignItems="center" style={{ marginLeft: "auto", marginRight: "1rem" }}> <DeleteIcon onClick={() => dispatch(deleteSoilElementFromGraph([projectId, fileName, elementIndex]))} /> <div onClick={() => handleOpen(elementIndex)} style={{ marginTop: "5px" }}> @@ -113,11 +114,11 @@ export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, han <CardContent> <Stack direction="column" justifyContent="flex-start" alignItems="flex-start" spacing={2}> <TextField - onBlur={() => dispatch(setGraphModelCard([projectId, fileName, elementIndex, parameterRef.current]))} + onBlur={() => dispatch(setGraphModelCard([projectId, fileName, elementIndex, variableRef.current]))} id="name" label="Name" onChange={handleChange} - value={parameterRef.current.name} + value={variableRef.current.name} /> <TextField style={{ width: "100%" }} @@ -125,12 +126,12 @@ export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, han label="Description" multiline onChange={handleChange} - value={parameterRef.current.description} + value={variableRef.current.description} /> <FormControl fullWidth> <InputLabel>Datatype</InputLabel> - <Select label="Datatype" value={parameterRef.current.datatype} + <Select label="Datatype" value={variableRef.current.datatype} onChange={handleDatatypeChange}> <MenuItem value="boolean">Boolean</MenuItem> <MenuItem value="int">Integer</MenuItem> @@ -141,20 +142,14 @@ export function ParameterCard({ projectId, fileName, elementIndex, cardOpen, han </Select> </FormControl> <TextField - style={{ display: ("stringenumbooleantime".includes(parameterRef.current.datatype)) ? "none" : "block" }} + style={{ display: ("stringenumbooleantime".includes(variableRef.current.datatype)) ? "none" : "block" }} id="unit" label="Unit" onChange={handleChange} - value={parameterRef.current.unit} - /> - <TextField - id="default" - label="Default" - onChange={handleChange} - value={parameterRef.current.default} + value={variableRef.current.unit} /> <DimensionInput numbers={dimension} onChange={(newNumbers: number[]) => handleDimensionChange(newNumbers)} /> - <InputRange projectId={projectId} fileName={fileName} enumValues={parameterRef.current.datatype === "enum" ? generateValidEnums() : []} label="Range" rangeValue={parameterRef.current.range} datatype={parameterRef.current.datatype} onChange={handleRangeChange}/> + <InputRange projectId={projectId} fileName={fileName} enumValues={variableRef.current.datatype === "enum" ? generateValidEnums() : []} label="Range" rangeValue={variableRef.current.range} datatype={variableRef.current.datatype} onChange={handleRangeChange}/> </Stack> </CardContent> </Collapse> diff --git a/src/features/soil-editor/SoilCardList/SoilCards/VariableValueInput.tsx b/src/features/soil-editor/SoilCardList/SoilCards/VariableValueInput.tsx new file mode 100644 index 0000000000000000000000000000000000000000..c9bdeb22d93e44ac09b8e4a3ec75dccdda9000b0 --- /dev/null +++ b/src/features/soil-editor/SoilCardList/SoilCards/VariableValueInput.tsx @@ -0,0 +1,162 @@ +import React, { useState, useEffect } from 'react' +import { RangeValue } from './RangeInput' +import { Checkbox, FormControlLabel, Switch, TextField } from '@mui/material' + +type Props = { + datatype: string, + range?: RangeValue, + onChange: (value: string | number | boolean, error: boolean) => void, +} + +export const VariableValueInput = ({datatype, range, onChange}: Props) => { + const [boolChecked, setBoolChecked] = useState(false); + const [boolInitialValue, setBoolInitialValue] = useState(true); + const [textFieldValue, setTextFieldValue] = useState(""); + const [wrongDatatypeError, setWrongDatatypeError] = useState(false); + const [outOfRangeError, setOutOfRangeError] = useState(false); + const [helperText, setHelperText] = useState(""); + + console.log(datatype) + + useEffect(() => { + if (range !== undefined) { + if (range[0] !== undefined && range[1] !== undefined) setHelperText("range: [" + range[0] + ", " + range[1] + "]"); + } + },[]) + + useEffect(() => { + if (wrongDatatypeError) { + setHelperText("Wrong datatype provided!") + } + else { + if(outOfRangeError) { + setHelperText("Provided value is out of specified range"); + } + else { + if(range !== undefined) { + if (range[0] !== undefined && range[1] !== undefined) setHelperText("range: [" + range[0] + ", " + range[1] + "]"); + else setHelperText(""); + } + else { + setHelperText(""); + } + } + } + }, [wrongDatatypeError, outOfRangeError]); + + const validateInput = (value: string) => { + switch(datatype) { + case "string": { + const regex = new RegExp(/^".*"$/); + if (!regex.test(value)) { + setWrongDatatypeError(true); + return false; + } + setWrongDatatypeError(false); + return true; + } + case "float": { + console.log("Regex Value is:") + const regex = new RegExp(/^-?\d+(\.\d+)?$/); + console.log(regex.test(value)); + if (!regex.test(value)) { + setWrongDatatypeError(true); + return false; + } + setWrongDatatypeError(false); + if (range !== undefined) { + const valueAsNumber = Number(value); + if (valueAsNumber < range[0] || valueAsNumber > range[1]) { + setOutOfRangeError(true); + return false; + } + } + setOutOfRangeError(false); + return true; + } + case "int": { + console.log(value); + const regex = new RegExp(/^-?\d+$/); + console.log(regex.test(value)); + if (!regex.test(value)) { + setWrongDatatypeError(true); + return false; + } + setWrongDatatypeError(false); + if (range !== undefined) { + const valueAsNumber = Number(value); + if (valueAsNumber < range[0] || valueAsNumber > range[1]) { + setOutOfRangeError(true); + return false; + } + } + setOutOfRangeError(false); + return true; + } + default: { + return true; + } + } + } + + const handleTextFieldValueChange = (e: React.ChangeEvent<HTMLInputElement>) => { + const tfVal = e.target.value; + setTextFieldValue(tfVal) + console.log(tfVal); + + // Special Case, if the field is empty return early, so no error is raised + if (tfVal === "") { + setWrongDatatypeError(false); + setOutOfRangeError(false); + onChange(tfVal, false); + return; + } + + const valid = validateInput(tfVal); + if (valid) { + + switch(datatype) { + case "int": { + onChange(Number(tfVal), false); + break; + } + case "float": { + onChange(Number(tfVal), false); + break; + } + default: onChange(tfVal, false) + } + } + else { + onChange(tfVal, true); + } + } + + const handleBoolSwitchChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setBoolInitialValue(e.target.checked); + onChange(e.target.checked, false); + } + + const handleBoolCheckboxChange = (e: React.ChangeEvent<HTMLInputElement>) => { + setBoolChecked(e.target.checked); + if (e.target.checked === true) { + onChange(boolInitialValue, false); + } + } + + switch(datatype) { + case "boolean": { + return ( + <div> + <FormControlLabel control={<Checkbox checked={boolChecked} onChange={handleBoolCheckboxChange}/>} label="Initial Value?" /> + <Switch checked={boolInitialValue} disabled={!boolChecked} onChange={handleBoolSwitchChange}/> + </div> + ) + } + default: { + return ( + <TextField error={wrongDatatypeError || outOfRangeError} helperText={helperText} value={textFieldValue} onChange={handleTextFieldValueChange} style={{ marginTop: "1.5rem" }} label="Initial Value (optional)"/> + ) + } + } +} diff --git a/src/features/soil-editor/SoilEditor.tsx b/src/features/soil-editor/SoilEditor.tsx index c025b9faf4fae32511735f5a43835fc29ee37867..31f314512f4a3c79a4c396b35608ecace8b3be00 100644 --- a/src/features/soil-editor/SoilEditor.tsx +++ b/src/features/soil-editor/SoilEditor.tsx @@ -59,11 +59,13 @@ export function SoilEditor() { // In order to implement that, I'd apply my type to the hook when calling it. const { projectId } = useParams<EditorParams>(); - const showTextModel = soilProjects[projectId].showTextModel; if (soilProjects[projectId] === undefined) { return <Redirect to="/"></Redirect> } + + const showTextModel = soilProjects[projectId].showTextModel; + return ( <Stack display={"flex"} direction={'column'} justifyContent="flex-start" spacing={1}> <Paper elevation={2} > diff --git a/src/features/soil-editor/SoilToolbar/PublishModal.tsx b/src/features/soil-editor/SoilToolbar/PublishModal.tsx index dd808b7d834470b3680729bddab090ee07ef5509..eb702752c51ccf8c926713cdcc9b615b9c2722a6 100644 --- a/src/features/soil-editor/SoilToolbar/PublishModal.tsx +++ b/src/features/soil-editor/SoilToolbar/PublishModal.tsx @@ -2,14 +2,17 @@ import CloudUploadIcon from '@mui/icons-material/CloudUpload'; import { Box, Button, Dialog, DialogActions, DialogContent, DialogTitle, Stack, TextField } from '@mui/material'; import * as React from 'react'; import { DATA_BACKEND } from '../../../const'; -import { useAppDispatch } from '../../../store/hooks'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; import { publishProjectById, validateModelById, } from '../soileditorSlice'; +import { usertokenState } from '../../../app/Layout/Navigation/usertokenSlice'; export default function PublishModal(props: { projectId: string, hasError: boolean }) { const dispatch = useAppDispatch(); const [open, setOpen] = React.useState(false); const [version, setVersion] = React.useState("1.0"); const [name, setName] = React.useState("My Project"); + const [invalidVersion, setInvalidVersion] = React.useState(false); + const token = useAppSelector(usertokenState); const handleOpen = () => { setOpen(true) @@ -36,9 +39,23 @@ export default function PublishModal(props: { projectId: string, hasError: boole }; const handleClose = () => setOpen(false); + const handlePublish = () => { + if (token.logged_in && token.token) dispatch(publishProjectById([props.projectId, name, version, token.token])); + handleClose(); + } + + const handleVersionChange = (e: React.ChangeEvent<HTMLTextAreaElement | HTMLInputElement>) => { + const version = e.target.value; + if (version.match(/^(\d+\.\d+)(\.\d+)*$/)) { + setInvalidVersion(false); + } + else setInvalidVersion(true); + setVersion(e.target.value); + } + return ( <div> - <Button startIcon={<CloudUploadIcon />} onClick={handleOpen} disabled={true}> + <Button startIcon={<CloudUploadIcon />} onClick={handleOpen} disabled={false}> Publish </Button> <Dialog @@ -49,12 +66,13 @@ export default function PublishModal(props: { projectId: string, hasError: boole <DialogContent> <Stack spacing={2} marginTop={1}> <TextField label="Name" value={name} onChange={(e) => setName(e.target.value)}></TextField> - <TextField disabled label="Version" value={version} onChange={(e) => setVersion(e.target.value)}></TextField> + <TextField label="Version" value={version} onChange={handleVersionChange}></TextField> {props.hasError && <Box color="error.main">Please Fix the Projects Errors before publishing</Box>} + {invalidVersion && <Box color="error.main">Provided version is invalid</Box>} </Stack> <DialogActions> <Button onClick={handleClose}>Abort</Button> - <Button disabled={props.hasError} onClick={() => { dispatch(publishProjectById([props.projectId, name, version])); handleClose() }} autoFocus> + <Button disabled={props.hasError || invalidVersion} onClick={handlePublish} autoFocus> Publish </Button> </DialogActions> diff --git a/src/features/soil-editor/TopNavigationBar/DeleteFileDialog.tsx b/src/features/soil-editor/TopNavigationBar/DeleteFileDialog.tsx index 01d33f4b546d01d79603ec2863c3f400c54f9426..63361d3292363171760903d914881dc39ff53829 100644 --- a/src/features/soil-editor/TopNavigationBar/DeleteFileDialog.tsx +++ b/src/features/soil-editor/TopNavigationBar/DeleteFileDialog.tsx @@ -5,14 +5,25 @@ import DialogActions from '@mui/material/DialogActions'; import DialogContent from '@mui/material/DialogContent'; import DialogContentText from '@mui/material/DialogContentText'; import DialogTitle from '@mui/material/DialogTitle'; +import { Project, saveProject, saveProjectById, selectPrivateProjects } from '../soileditorSlice'; +import { useEffect, useState } from 'react'; +import { useAppDispatch, useAppSelector } from '../../../store/hooks'; +import SaveIcon from '@mui/icons-material/Save'; +import SaveButton from '../SoilToolbar/SaveButton'; +import { usertokenState } from '../../../app/Layout/Navigation/usertokenSlice'; type DeleteFileDialogProps = { open: boolean, setOpen: Function, filename: string, deleteFile: Function, + project?: Project, } -export default function DeleteFileDialog({ open, setOpen, filename, deleteFile }: DeleteFileDialogProps) { +export default function DeleteFileDialog({ open, setOpen, filename, deleteFile, project }: DeleteFileDialogProps) { + const privateProjects = useAppSelector(selectPrivateProjects); + const [unsavedChanges, setUnsavedChanges] = useState(false); + const dispatch = useAppDispatch(); + const token = useAppSelector(usertokenState) const handleCloseDelete = () => { deleteFile(filename); @@ -23,6 +34,29 @@ export default function DeleteFileDialog({ open, setOpen, filename, deleteFile } setOpen(false); }; + const handleSaveAndDelete = () => { + if (project === undefined) return; + dispatch(saveProject(project.projectId)); + + if (token.logged_in && token.token) dispatch(saveProjectById([project.projectId, project.name, project.version, token.token])); + + setUnsavedChanges(false); + handleCloseDelete(); + } + + useEffect(() => { + console.log("I'm executed!") + console.log(project); + if(project !== undefined && project.projectId in privateProjects) { + console.log("Test") + Object.entries(project.files).map(([fileName, file]) => { + if(file.changed) { + setUnsavedChanges(true); + } + }); + } + }, [project]) + return ( <div> <Dialog @@ -38,8 +72,15 @@ export default function DeleteFileDialog({ open, setOpen, filename, deleteFile } <DialogContentText id="alert-dialog-description"> Are you sure you want to delete {filename} ? </DialogContentText> + {unsavedChanges && + <DialogContentText sx={{color:"InfoText", fontStyle:"italic", mt:3}}> + Warning: You have unsaved changes inside your project! + </DialogContentText>} </DialogContent> <DialogActions> + {unsavedChanges && project && + <Button variant="contained" startIcon={<SaveIcon />} onClick={handleSaveAndDelete}>Save & Delete</Button> + } <Button variant="contained" onClick={handleClose} autoFocus> Keep </Button> diff --git a/src/features/soil-editor/TopNavigationBar/TopNavigationBar.tsx b/src/features/soil-editor/TopNavigationBar/TopNavigationBar.tsx index 631b8b83fc7fa54ad835c011012b1330d0a6f52c..eb757f30581e732ec5ae43be28ba662a18c6cd61 100644 --- a/src/features/soil-editor/TopNavigationBar/TopNavigationBar.tsx +++ b/src/features/soil-editor/TopNavigationBar/TopNavigationBar.tsx @@ -73,7 +73,7 @@ export default function TopNavigationBar({ files }: TopNavigationBarProps) { </Tabs> </Box> - <DeleteFileDialog open={deleteDialogOpen} setOpen={setDeleteDialogOpen} deleteFile={deleteFile} filename={fileToDelete} /> + <DeleteFileDialog open={deleteDialogOpen} setOpen={setDeleteDialogOpen} deleteFile={deleteFile} filename={fileToDelete} /> </Box> ); } \ No newline at end of file diff --git a/src/features/soil-editor/soileditorSlice.ts b/src/features/soil-editor/soileditorSlice.ts index e9e1a0032c5afff6dc97dac6e25ad1ee891d6ad8..9c363f7cc888a91198051b9c340892be9fdacd8c 100644 --- a/src/features/soil-editor/soileditorSlice.ts +++ b/src/features/soil-editor/soileditorSlice.ts @@ -13,7 +13,7 @@ export type GenerationSetting = { } // Define the element types and data types -type ElementType = "enum" | "measurement" | "parameter" | "function" | "component" | "interface"; +type ElementType = "enum" | "measurement" | "parameter" | "function" | "variable" | "component" | "interface"; type DataType = "boolean" | "int" | "string" | "enum" | "time" | "float"; // Define common attributes for different element types @@ -59,20 +59,38 @@ export type SoilParameter = CommonAttributes & { constant: boolean; }; +// Define the SoilVariable type +export type SoilVariable = CommonAttributes & { + elementType: "variable"; + description: string; + dimension: [number | undefined] | []; + datatype: DataType; + range?: [any, any]; + unit: string; + constant: boolean; +}; + // Define the SoilFunction type export type SoilFunction = CommonAttributes & { elementType: "function"; description: string; - arguments: (NamedElement<SoilParameter> | NamedElement<SoilMeasurement>)[]; - returns: (NamedElement<SoilParameter> | NamedElement<SoilMeasurement>)[]; + arguments: (NamedElement<SoilParameter> | NamedElement<SoilMeasurement> | NamedElement<SoilVariable>)[]; + returns: (NamedElement<SoilParameter> | NamedElement<SoilMeasurement> | NamedElement<SoilVariable>)[]; }; // Define the NamedElement type export type NamedElement<T> = { name: string; value: T; + typeName: string; + customName?: string; // Attribute for when the typename is overwritten + description?: string; // Attribute for when the description is overwritten constant?: boolean; + internal?: boolean; + streaming?: boolean; dynamic?: boolean; + overrides?: Map<string, string>; + initialValue?: string | number | boolean; }; // Define the SoilComponent type @@ -117,7 +135,7 @@ export type File = { cardsOpen: boolean[]; fileName: string; textModel: string; - graphModel: (SoilMeasurement | SoilParameter | SoilFunction | SoilComponent | SoilEnum | SoilInterface)[]; + graphModel: ( SoilVariable | SoilFunction | SoilComponent | SoilEnum | SoilInterface)[]; generationReport: { errors: Error[]; warnings: Object[]; @@ -147,6 +165,9 @@ export interface SoileditorState { }; projects: { [key: string]: Project; + }, + privateProjects: { + [key: string]: Project; }; } @@ -186,6 +207,7 @@ const initialState: SoileditorState = { severity: "success", }, projects: {}, + privateProjects: {} }; /* @@ -199,12 +221,13 @@ These async actions interact with the server and handle the project publishing, // Define the publishProjectById async action export const publishProjectById = createAsyncThunk<string, string[], { state: RootState }>( 'projects/publishProjectById', - async ([projectId, name, version], thunkAPI) => { + async ([projectId, name, version, usertoken], thunkAPI) => { const state = thunkAPI.getState().soileditor; var myHeaders = new Headers(); myHeaders.append("Content-Type", "application/json"); - let response = await fetch(DATA_BACKEND + "/projects", { + myHeaders.append("Authorization", usertoken); + let response = await fetch(DATA_BACKEND + "/publicProjects", { method: 'POST', headers: myHeaders, body: JSON.stringify({ id: projectId, project: state.projects[projectId], version: version, projectName: name }), @@ -212,6 +235,7 @@ export const publishProjectById = createAsyncThunk<string, string[], { state: Ro }) .then(rsp => rsp.text()) .catch(error => error.text()); + console.log(response); return response; } ); @@ -252,6 +276,8 @@ export const validateModelById = createAsyncThunk<GenerationReportResponse, stri async (projectId, thunkAPI) => { const state = thunkAPI.getState().soileditor; + + // TODO: Does this assume that the first file in state.projects is the main file? let fData = new FormData(); let fName = ""; if (state.projects[projectId].showTextModel) { @@ -308,6 +334,7 @@ export const deleteProjectById = createAsyncThunk<string, string, { state: RootS async (projectId, thunkAPI) => { var myHeaders = new Headers(); const usertoken = thunkAPI.getState().usertoken.token; + const privateProjects = thunkAPI.getState().soileditor.privateProjects; if (usertoken === null) return; myHeaders.append("Content-Type", "application/json"); myHeaders.append("Authorization", usertoken); @@ -316,7 +343,13 @@ export const deleteProjectById = createAsyncThunk<string, string, { state: RootS headers: myHeaders, redirect: 'follow', }) - .then(rsp => rsp.text()) + .then(rsp => { + rsp.text(); + if(rsp.status === 200) { + console.log("Delete returned 200") + thunkAPI.dispatch(removePrivateProject(projectId)); + } + }) .catch(error => error.text()); return response; } @@ -356,6 +389,7 @@ export const soileditorSlice = createSlice({ reducers: { // Add a new project to the state with a given text model and filename. addTextModel: (state, action: PayloadAction<string[]>) => { + console.log("test123)") const newUUID = uuidv4() state.projects[newUUID] = { name: action.payload[0], projectId: newUUID, files: {}, version: "1.0", showTextModel: true, loading: false, timestamp: new Date().toISOString() } const files = state.projects[newUUID].files; @@ -371,10 +405,15 @@ export const soileditorSlice = createSlice({ state.projects[action.payload].showTextModel = !state.projects[action.payload].showTextModel }, // Import a project into the state. - importProject: (state, action: PayloadAction<Project>) => { + importPublicProject: (state, action: PayloadAction<Project>) => { const newUUID = uuidv4() - action.payload.projectId = newUUID; - state.projects[newUUID] = action.payload + var project = {...action.payload} + project.projectId = newUUID; + state.projects[newUUID] = project; + }, + importPrivateProject: (state, action: PayloadAction<Project>) => { + console.log(action.payload); + state.projects[action.payload.projectId] = action.payload; }, // Add a new file to a specific project in the state. addFileToProject: (state, action: PayloadAction<string[]>) => { @@ -392,12 +431,19 @@ export const soileditorSlice = createSlice({ removeProject: (state, action: PayloadAction<string>) => { delete state.projects[action.payload] }, + removePrivateProject: (state, action: PayloadAction<string>) => { + delete state.privateProjects[action.payload]; + }, + clearPrivateProjects: (state, action: PayloadAction<void>) => { + state.privateProjects = {} + }, // Remove a file from a specific project in the state. removeFileFromProject: (state, action: PayloadAction<string[]>) => { delete state.projects[action.payload[0]].files[action.payload[1]] }, // Set the graph model for a specific file in a project. setGraphModel: (state, action: PayloadAction<any[]>) => { + console.log(action.payload); state.projects[action.payload[0]].files[action.payload[1]].graphModel = action.payload[2]; state.projects[action.payload[0]].loading = false; }, @@ -470,13 +516,12 @@ export const soileditorSlice = createSlice({ }; state.projects[action.payload[0]].files[action.payload[1]].graphModel.push(newComponent) }, - // Add a new parameter to the graph model of a specific file in a project. - addParameterToFile: (state, action: PayloadAction<any[]>) => { - let newComponent: SoilParameter = { + addVariableToFile: (state, action: PayloadAction<any[]>) => { + let newComponent: SoilVariable = { cardOpen: false, - elementType: "parameter", - name: "New Parameter", - description: "A New Parameter.", + elementType: "variable", + name: "New Variable", + description: "A New Variable.", dimension: [], datatype: "int", range: [0, 1], @@ -487,21 +532,6 @@ export const soileditorSlice = createSlice({ }; state.projects[action.payload[0]].files[action.payload[1]].graphModel.push(newComponent) }, - // Add a new measurement to the graph model of a specific file in a project. - addMeasurementToFile: (state, action: PayloadAction<any[]>) => { - let newComponent: SoilMeasurement = { - elementType: "measurement", - name: "New Measurement", - cardOpen: false, - description: "A New Measurement.", - dimension: [], - datatype: "int", - range: [0, 1], - unit: "CEL", - uuid: uuidv4() - }; - state.projects[action.payload[0]].files[action.payload[1]].graphModel.push(newComponent) - }, // Add a new enumeration to the graph model of a specific file in a project. addEnumToFile: (state, action: PayloadAction<any[]>) => { let newComponent: SoilEnum = { @@ -519,7 +549,7 @@ export const soileditorSlice = createSlice({ saveProject: (state, action: PayloadAction<string>) => { Object.entries(state.projects[action.payload].files).forEach(([filename, file]) => file.changed = false); state.projects[action.payload].timestamp = new Date().toISOString(); - } + }, }, extraReducers: (builder) => { // Add a case for when the `generateModelById` action is fulfilled @@ -573,14 +603,21 @@ export const soileditorSlice = createSlice({ // Add a case for when the `publishProjectById` action is fulfilled builder.addCase(publishProjectById.fulfilled, (state, action) => { let result = JSON.parse(action.payload) + console.log(result); // Display a snackbar with a message indicating if publishing the project succeeded or failed - if (result[0] === "Success") { + if (result[1] === 201) { state.snackbarInfo.severity = "success"; state.snackbarInfo.content = "The Project has been published successfully." } else { - state.snackbarInfo.severity = "error"; - state.snackbarInfo.content = "An error has occured while publishing the Project. " + if (result[1] === 409) { + state.snackbarInfo.severity = "error"; + state.snackbarInfo.content = "A project with the same id and version is already public!" + } + else { + state.snackbarInfo.severity = "error"; + state.snackbarInfo.content = "An error has occured while publishing the Project. " + } } state.snackbarInfo.open = true }); @@ -642,11 +679,13 @@ export const soileditorSlice = createSlice({ state.snackbarInfo.open = true; }); builder.addCase(deleteProjectById.fulfilled, (state, action) => { + console.log("A") state.snackbarInfo.severity = "success"; state.snackbarInfo.content = "Project deleted successfully."; state.snackbarInfo.open = true; }); builder.addCase(deleteProjectById.rejected, (state, action) => { + console.log("B") state.snackbarInfo.severity = "error"; state.snackbarInfo.content = "There was a problem deleting your project"; state.snackbarInfo.open = true; @@ -658,21 +697,22 @@ export const soileditorSlice = createSlice({ }); builder.addCase(getProjectsFromDatabase.fulfilled, (state, action) => { action.payload.forEach(proj => { - if(state.projects[proj.id] !== undefined) { + if(state.privateProjects[proj.id] !== undefined) { // TODO: implement logic to ask if user wants newer project state instead of just overwriting the local state - if(state.projects[proj.id].timestamp < proj.project.timestamp) state.projects[proj.id] = proj.project; + if(state.privateProjects[proj.id].timestamp < proj.project.timestamp) state.projects[proj.id] = proj.project; } else { - state.projects[proj.id] = proj.project; + state.privateProjects[proj.id] = proj.project; } }); }); }, }); -export const { addTextModel, addFileToProject, removeFileFromProject, setGraphModel, setImports, addEnumToFile, addFunctionToFile, addMeasurementToFile, addParameterToFile, addComponentToFile, deleteSoilElementFromGraph, addImportToFile, removeImportFromFile, setCardsOpen, toggleSnackbar, changeTextOfFile, importProject, removeProject, setGraphModelCard, toggleModelRepresentation, saveProject } = soileditorSlice.actions; +export const { addTextModel, addFileToProject, removeFileFromProject, setGraphModel, setImports, addEnumToFile, addFunctionToFile, addVariableToFile, addComponentToFile, deleteSoilElementFromGraph, addImportToFile, removeImportFromFile, setCardsOpen, toggleSnackbar, changeTextOfFile, importPublicProject, importPrivateProject, removeProject, removePrivateProject, clearPrivateProjects, setGraphModelCard, toggleModelRepresentation, saveProject } = soileditorSlice.actions; export const selectProjects = (state: RootState) => state.soileditor.projects; +export const selectPrivateProjects = (state: RootState) => state.soileditor.privateProjects; export const selectSnackbarInfo = (state: RootState) => state.soileditor.snackbarInfo; diff --git a/src/router/routes.ts b/src/router/routes.ts index c0c31320f9c685b2fcd31d0177305d187623c0b4..55461cbeb95b2977aea877319d113d4fa9ebd6b0 100644 --- a/src/router/routes.ts +++ b/src/router/routes.ts @@ -2,6 +2,8 @@ import { FC } from 'react'; import { SoilEditor } from '../features/soil-editor/SoilEditor'; import { PublicProjects } from '../features/publicProjects/PublicProjects'; import { Homepage } from '../features/homepage/Homepage'; +import { PrivateProjects } from '../features/privateProjects/PrivateProjects'; +import { DemoProjects } from '../features/demoProjects/DemoProjects'; // Define the Route type type Route = { @@ -29,6 +31,16 @@ const routes: Route[] = [ path: '/public', Component: PublicProjects, }, + { + name: 'Private Projects', + path: '/private', + Component: PrivateProjects, + }, + { + name: 'Demo Projects', + path: '/demo', + Component: DemoProjects, + } ]; // Export the routes array