diff --git a/csharp/App/Backend/DataTypes/Methods/Session.cs b/csharp/App/Backend/DataTypes/Methods/Session.cs
index 110b9ab1b..b9aee8351 100644
--- a/csharp/App/Backend/DataTypes/Methods/Session.cs
+++ b/csharp/App/Backend/DataTypes/Methods/Session.cs
@@ -14,9 +14,9 @@ public static class SessionMethods
var user = session?.User;
return user is not null
- && folder is not null
- && user.UserType!=0
- && user.HasAccessTo(folder.Parent())
+ && folder is not null
+ && user.UserType==2
+ && user.HasAccessTo(folder.Parent())
&& Db.Create(folder) // TODO: these two in a transaction
&& Db.Create(new FolderAccess { UserId = user.Id, FolderId = folder.Id });
}
@@ -43,8 +43,8 @@ public static class SessionMethods
var parent = Db.GetFolderById(parentId);
return user is not null
- && folder is not null
- && user.UserType !=0
+ && folder is not null
+ && user.UserType==2
&& user.HasAccessTo(folder)
&& user.HasAccessTo(parent)
&& folder
@@ -61,7 +61,7 @@ public static class SessionMethods
if(installation == null || installation.ParentId == parentId) return false;
return user is not null
- && user.UserType !=0
+ && user.UserType==2
&& user.HasAccessTo(installation)
&& user.HasAccessTo(parent)
&& installation
diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/CustomTreeItem.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/CustomTreeItem.tsx
index 036540aa7..469d9658d 100644
--- a/typescript/frontend-marios2/src/content/dashboards/Tree/CustomTreeItem.tsx
+++ b/typescript/frontend-marios2/src/content/dashboards/Tree/CustomTreeItem.tsx
@@ -1,15 +1,19 @@
import React, { ReactNode, useContext, useState } from 'react';
-import { CircularProgress, ListItemIcon, useTheme } from '@mui/material';
+import { CircularProgress, IconButton, ListItemIcon, useTheme } from '@mui/material';
import { TreeItem } from '@mui/lab';
import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
import FolderIcon from '@mui/icons-material/Folder';
import InsertDriveFileIcon from '@mui/icons-material/InsertDriveFile';
+import DriveFileMoveOutlinedIcon from '@mui/icons-material/DriveFileMoveOutlined';
import Typography from '@mui/material/Typography';
import { makeStyles } from '@mui/styles';
import CancelIcon from '@mui/icons-material/Cancel';
import routes from 'src/Resources/routes.json';
import { useLocation, useNavigate } from 'react-router-dom';
import { ProductIdContext } from '../../../contexts/ProductIdContextProvider';
+import { UserContext } from '../../../contexts/userContext';
+import { UserType } from '../../../interfaces/UserTypes';
+import MoveModal from './MoveModal';
interface CustomTreeItemProps {
node: I_Installation | I_Folder;
@@ -41,8 +45,10 @@ function CustomTreeItem(props: CustomTreeItemProps) {
const status = props.node.status;
const navigate = useNavigate();
const [selected, setSelected] = useState(false);
+ const [openMoveModal, setOpenMoveModal] = useState(false);
const currentLocation = useLocation();
const { product } = useContext(ProductIdContext);
+ const { currentUser } = useContext(UserContext);
const handleSelectOneInstallation = (): void => {
let installation = props.node;
@@ -126,6 +132,24 @@ function CustomTreeItem(props: CustomTreeItemProps) {
{props.node.name}
+ {currentUser.userType === UserType.admin && (
+
e.stopPropagation()}>
+ setOpenMoveModal(true)}
+ sx={{ ml: 1 }}
+ >
+
+
+ {openMoveModal && (
+ setOpenMoveModal(false)}
+ />
+ )}
+
+ )}
+
{props.node.type === 'Installation' && (
{status === -1 ? (
diff --git a/typescript/frontend-marios2/src/content/dashboards/Tree/MoveModal.tsx b/typescript/frontend-marios2/src/content/dashboards/Tree/MoveModal.tsx
new file mode 100644
index 000000000..b822fbb34
--- /dev/null
+++ b/typescript/frontend-marios2/src/content/dashboards/Tree/MoveModal.tsx
@@ -0,0 +1,165 @@
+import React, { useContext, useState } from 'react';
+import {
+ Alert,
+ Box,
+ CircularProgress,
+ FormControl,
+ IconButton,
+ InputLabel,
+ MenuItem,
+ Modal,
+ Select,
+ Typography,
+ useTheme
+} from '@mui/material';
+import Button from '@mui/material/Button';
+import { Close as CloseIcon } from '@mui/icons-material';
+import { I_Folder, I_Installation } from 'src/interfaces/InstallationTypes';
+import { InstallationsContext } from 'src/contexts/InstallationsContextProvider';
+import { ProductIdContext } from 'src/contexts/ProductIdContextProvider';
+import { FormattedMessage } from 'react-intl';
+
+interface MoveModalProps {
+ node: I_Installation | I_Folder;
+ onClose: () => void;
+}
+
+// Returns the IDs of a folder and all its descendants (to prevent circular moves)
+function getDescendantIds(folderId: number, allItems: any[]): Set
{
+ const result = new Set([folderId]);
+ const queue = [folderId];
+ while (queue.length > 0) {
+ const currentId = queue.shift();
+ allItems
+ .filter((item) => item.parentId === currentId && item.type === 'Folder')
+ .forEach((child) => {
+ result.add(child.id);
+ queue.push(child.id);
+ });
+ }
+ return result;
+}
+
+function MoveModal(props: MoveModalProps) {
+ const theme = useTheme();
+ const { foldersAndInstallations, moveInstallation, moveFolder, loading, setLoading, error, setError } =
+ useContext(InstallationsContext);
+ const { product } = useContext(ProductIdContext);
+ const [selectedFolderId, setSelectedFolderId] = useState('');
+
+ // For folders: exclude self and all descendants to prevent circular nesting
+ // For installations: any folder is valid
+ const excludedIds =
+ props.node.type === 'Folder'
+ ? getDescendantIds(props.node.id, foldersAndInstallations)
+ : new Set();
+
+ const availableFolders = foldersAndInstallations.filter(
+ (item) => item.type === 'Folder' && !excludedIds.has(item.id)
+ );
+
+ const handleSubmit = async () => {
+ if (selectedFolderId === '') return;
+ setLoading(true);
+ setError(false);
+ if (props.node.type === 'Installation') {
+ await moveInstallation(props.node.id, selectedFolderId as number, product);
+ } else {
+ await moveFolder(props.node.id, selectedFolderId as number, product);
+ }
+ setLoading(false);
+ props.onClose();
+ };
+
+ return (
+ {}}
+ aria-labelledby="move-modal"
+ >
+
+
+ :{' '}
+
+ {props.node.name}
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {loading && (
+
+ )}
+
+ {error && (
+
+
+ setError(false)}
+ sx={{ marginLeft: '4px' }}
+ >
+
+
+
+ )}
+
+
+
+ );
+}
+
+export default MoveModal;
diff --git a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx
index 909b68e5f..939ad344f 100644
--- a/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx
+++ b/typescript/frontend-marios2/src/contexts/InstallationsContextProvider.tsx
@@ -349,6 +349,42 @@ const InstallationsContextProvider = ({
[fetchAllFoldersAndInstallations, navigate, removeToken]
);
+ const moveInstallation = useCallback(
+ async (installationId: number, parentId: number, product: number) => {
+ try {
+ await axiosConfig.put(
+ `/MoveInstallation?installationId=${installationId}&parentId=${parentId}`
+ );
+ await fetchAllFoldersAndInstallations(product);
+ } catch (error) {
+ setError(true);
+ if (error.response?.status === 401) {
+ removeToken();
+ navigate(routes.login);
+ }
+ }
+ },
+ [fetchAllFoldersAndInstallations, navigate, removeToken]
+ );
+
+ const moveFolder = useCallback(
+ async (folderId: number, parentId: number, product: number) => {
+ try {
+ await axiosConfig.put(
+ `/MoveFolder?folderId=${folderId}&parentId=${parentId}`
+ );
+ await fetchAllFoldersAndInstallations(product);
+ } catch (error) {
+ setError(true);
+ if (error.response?.status === 401) {
+ removeToken();
+ navigate(routes.login);
+ }
+ }
+ },
+ [fetchAllFoldersAndInstallations, navigate, removeToken]
+ );
+
const contextValue = useMemo(
() => ({
salimax_or_sodistore_Installations,
@@ -369,6 +405,8 @@ const InstallationsContextProvider = ({
createFolder,
updateFolder,
deleteFolder,
+ moveInstallation,
+ moveFolder,
//currentProduct,
socket,
openSocket,