diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
index d65e09c2440397c9e7d9d9c22c264be36f3242b0..ad4d2e900ee9b61fa15807c3c2569199a6670773 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/controller/NodesController.java
@@ -8,7 +8,8 @@ package it.inaf.ia2.vospace.ui.controller;
 import it.inaf.ia2.aa.data.User;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import it.inaf.ia2.vospace.ui.data.ListNodeData;
-import it.inaf.ia2.vospace.ui.service.NodesHtmlGenerator;
+import it.inaf.ia2.vospace.ui.service.MainNodesHtmlGenerator;
+import it.inaf.ia2.vospace.ui.service.MoveNodeModalHtmlGenerator;
 import it.inaf.oats.vospace.datamodel.NodeUtils;
 import java.util.List;
 import java.util.Map;
@@ -33,6 +34,7 @@ import org.springframework.http.ResponseEntity;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
 
 @RestController
@@ -61,8 +63,23 @@ public class NodesController extends BaseController {
 
         listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
 
-        NodesHtmlGenerator htmlGenerator = new NodesHtmlGenerator(node, principal, authority);
-        listNodeData.setHtmlTable(htmlGenerator.generateNodes());
+        MainNodesHtmlGenerator htmlGenerator = new MainNodesHtmlGenerator(node, principal, authority);
+        listNodeData.setHtml(htmlGenerator.generateNodes());
+
+        return ResponseEntity.ok(listNodeData);
+    }
+
+    @GetMapping(value = "/nodesForMove", produces = MediaType.APPLICATION_JSON_VALUE)
+    public ResponseEntity<ListNodeData> listNodesForMoveModal(@RequestParam("path") String path, @RequestParam("nodeToMove") String nodeToMove, User principal) throws Exception {
+
+        ListNodeData listNodeData = new ListNodeData();
+
+        Node node = client.getNode(path);
+
+        listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
+
+        MoveNodeModalHtmlGenerator htmlGenerator = new MoveNodeModalHtmlGenerator(node, nodeToMove, principal, authority);
+        listNodeData.setHtml(htmlGenerator.generateNodes());
 
         return ResponseEntity.ok(listNodeData);
     }
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
index 72dc9eecd49c11ac5df7fe90b272c85e4e378f7e..9143b2b05c9d2db9efaaafc2df5ee8de629a7e72 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/data/ListNodeData.java
@@ -7,15 +7,15 @@ package it.inaf.ia2.vospace.ui.data;
 
 public class ListNodeData {
 
-    private String htmlTable;
+    private String html;
     private boolean writable;
 
-    public String getHtmlTable() {
-        return htmlTable;
+    public String getHtml() {
+        return html;
     }
 
-    public void setHtmlTable(String htmlTable) {
-        this.htmlTable = htmlTable;
+    public void setHtml(String html) {
+        this.html = html;
     }
 
     public boolean isWritable() {
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..f20357d4e64321eb1451db0e94cdd282be9b5ece
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MainNodesHtmlGenerator.java
@@ -0,0 +1,195 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.service;
+
+import it.inaf.ia2.aa.data.User;
+import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import net.ivoa.xml.vospace.v2.Node;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+public class MainNodesHtmlGenerator extends NodesHtmlGenerator {
+
+    public MainNodesHtmlGenerator(Node node, User user, String authority) {
+        super(node, user, authority);
+    }
+
+    @Override
+    protected Element createContainerElement(Document html) {
+        Element container = html.body().appendElement("tbody");
+        container.attr("id", "nodes");
+        return container;
+    }
+
+    @Override
+    protected void addChild(Node child, Element containerElement) {
+        NodeInfo nodeInfo = new NodeInfo(child, user, authority);
+
+        if (nodeInfo.isListOfFiles()) {
+            // hidden file
+            return;
+        }
+
+        Element row = containerElement.appendElement("tr");
+
+        addSelectionCell(nodeInfo, row);
+        addLinkCell(nodeInfo, row);
+        addSizeCell(nodeInfo, row);
+        addGroupReadCell(nodeInfo, row);
+        addGroupWriteCell(nodeInfo, row);
+        addActionsCell(nodeInfo, row);
+    }
+
+    private void addSelectionCell(NodeInfo nodeInfo, Element row) {
+
+        Element cell = row.appendElement("td");
+
+        Element input = cell.appendElement("input");
+        input.attr("type", "checkbox");
+        input.attr("data-node", nodeInfo.getPath());
+
+        if (nodeInfo.isAsyncTrans()) {
+            input.addClass("async");
+        } else if (nodeInfo.isDeletable()) {
+            input.addClass("deletable");
+        }
+    }
+
+    private void addLinkCell(NodeInfo nodeInfo, Element row) {
+
+        Element cell = row.appendElement("td");
+        addNodeIcon(nodeInfo, cell);
+        addLink(nodeInfo, cell);
+    }
+
+    private void addSizeCell(NodeInfo nodeInfo, Element row) {
+        Element cell = row.appendElement("td");
+        cell.text(nodeInfo.getSize());
+    }
+
+    private void addGroupReadCell(NodeInfo nodeInfo, Element row) {
+        Element cell = row.appendElement("td");
+        fillGroupCell(cell, nodeInfo.getGroupRead());
+    }
+
+    private void addGroupWriteCell(NodeInfo nodeInfo, Element row) {
+        Element cell = row.appendElement("td");
+        fillGroupCell(cell, nodeInfo.getGroupWrite());
+    }
+
+    private void fillGroupCell(Element cell, String groups) {
+        String[] values = groups.split(" ");
+        List<String> personGroups = new ArrayList<>();
+        List<String> peopleGroups = new ArrayList<>();
+        for (String value : values) {
+            if (!value.isBlank()) {
+                if (value.startsWith("people.")) {
+                    personGroups.add(value.substring("people.".length()).replace("\\.", "."));
+                } else {
+                    peopleGroups.add(value);
+                }
+            }
+        }
+        if (!personGroups.isEmpty()) {
+            Element personIcon = cell.appendElement("span");
+            personIcon.attr("class", "icon person-icon");
+            cell.appendText(String.join(" ", personGroups));
+            cell.append("&nbsp;");
+        }
+        if (!peopleGroups.isEmpty()) {
+            Element personIcon = cell.appendElement("span");
+            personIcon.attr("class", "icon people-icon");
+            cell.appendText(String.join(" ", peopleGroups));
+        }
+    }
+
+    private void addActionsCell(NodeInfo nodeInfo, Element row) {
+        Element cell = row.appendElement("td");
+
+        Element dotsMenu = cell.appendElement("span");
+        dotsMenu.attr("class", "dots-menu icon dots-menu-icon pointer");
+
+        Element dropdown = dotsMenu.appendElement("span");
+        dropdown.attr("class", "dots-menu-content dropdown-menu");
+
+        String nodePathJs = makeJsArg(nodeInfo.getPath());
+
+        if (nodeInfo.isWritable()) {
+            Element shareBtn = dropdown.appendElement("button");
+            shareBtn.text("Share");
+            shareBtn.attr("type", "button");
+            shareBtn.attr("class", "dropdown-item");
+            shareBtn.attr("onclick", "shareNode(" + nodePathJs
+                    + "," + makeJsArg(nodeInfo.getGroupRead())
+                    + "," + makeJsArg(nodeInfo.getGroupWrite()) + ")");
+        }
+        if (nodeInfo.isDeletable()) {
+            Element renameBtn = dropdown.appendElement("button");
+            renameBtn.text("Rename");
+            renameBtn.attr("type", "button");
+            renameBtn.attr("class", "dropdown-item");
+            renameBtn.attr("onclick", "renameNode(" + nodePathJs + ")");
+
+            Element moveBtn = dropdown.appendElement("button");
+            moveBtn.text("Move");
+            moveBtn.attr("type", "button");
+            moveBtn.attr("class", "dropdown-item");
+            moveBtn.attr("onclick", "moveNode(" + nodePathJs + ")");
+
+            Element deleteBtn = dropdown.appendElement("button");
+            deleteBtn.text("Delete");
+            deleteBtn.attr("type", "button");
+            deleteBtn.attr("class", "dropdown-item");
+            deleteBtn.attr("onclick", "deleteNode(" + nodePathJs + ")");
+        }
+    }
+
+    private void addLink(NodeInfo nodeInfo, Element cell) {
+        if (isDownloadable(nodeInfo, user)) {
+            Element link = cell.appendElement("a");
+            String href;
+            if (nodeInfo.isFolder()) {
+                href = "#/nodes" + urlEncodePath(nodeInfo.getPath());
+            } else {
+                href = "download" + urlEncodePath(nodeInfo.getPath());
+                link.attr("target", "blank_");
+            }
+            link.attr("href", href);
+            link.text(nodeInfo.getName());
+        } else {
+            cell.appendText(nodeInfo.getName());
+        }
+    }
+
+    private boolean isDownloadable(NodeInfo nodeInfo, User user) {
+        if (nodeInfo.isFile()) {
+            if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
+                return false;
+            }
+        }
+        if (nodeInfo.isPublic()) {
+            return true;
+        }
+
+        if (nodeInfo.getCreator().equals(user.getName())) {
+            return true;
+        }
+
+        if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
+            List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
+            for (String group : groupRead) {
+                if (user.getGroups().contains(group)) {
+                    return true;
+                }
+            }
+        }
+
+        return false;
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveNodeModalHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveNodeModalHtmlGenerator.java
new file mode 100644
index 0000000000000000000000000000000000000000..f83d8cdb61abf643d8b466fe9b92476021b645cb
--- /dev/null
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/MoveNodeModalHtmlGenerator.java
@@ -0,0 +1,51 @@
+/*
+ * This file is part of vospace-ui
+ * Copyright (C) 2021 Istituto Nazionale di Astrofisica
+ * SPDX-License-Identifier: GPL-3.0-or-later
+ */
+package it.inaf.ia2.vospace.ui.service;
+
+import it.inaf.ia2.aa.data.User;
+import net.ivoa.xml.vospace.v2.Node;
+import org.jsoup.nodes.Document;
+import org.jsoup.nodes.Element;
+
+public class MoveNodeModalHtmlGenerator extends NodesHtmlGenerator {
+
+    private final String nodeNodeMovePath;
+
+    public MoveNodeModalHtmlGenerator(Node node, String nodeNodeMovePath, User user, String authority) {
+        super(node, user, authority);
+        this.nodeNodeMovePath = nodeNodeMovePath;
+    }
+
+    @Override
+    protected Element createContainerElement(Document html) {
+        Element container = html.body().appendElement("div");
+        container.attr("id", "move-nodes");
+        container.attr("class", "list-group");
+        return container;
+    }
+
+    @Override
+    protected void addChild(Node child, Element containerElement) {
+        NodeInfo nodeInfo = new NodeInfo(child, user, authority);
+
+        if (!nodeInfo.isFolder() || nodeInfo.getPath().equals(nodeNodeMovePath)) {
+            return;
+        }
+
+        Element row = containerElement.appendElement("div");
+        row.addClass("list-group-item");
+
+        addNodeIcon(nodeInfo, row);
+        addLink(nodeInfo, row);
+    }
+
+    private void addLink(NodeInfo nodeInfo, Element cell) {
+        Element link = cell.appendElement("a");
+        link.attr("href", "#");
+        link.attr("onclick", "openNodeInMoveModal(event, " + makeJsArg(nodeInfo.getPath()) + ")");
+        link.text(nodeInfo.getName());
+    }
+}
diff --git a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
index 2d2d1200bd65aed593876a73e24f41e37a188492..0df31b92896e243166653c781a479e5083519618 100644
--- a/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
+++ b/vospace-ui-backend/src/main/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGenerator.java
@@ -6,25 +6,22 @@
 package it.inaf.ia2.vospace.ui.service;
 
 import it.inaf.ia2.aa.data.User;
-import static it.inaf.oats.vospace.datamodel.NodeUtils.urlEncodePath;
-import java.util.ArrayList;
-import java.util.Arrays;
-import java.util.List;
 import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.Node;
 import org.jsoup.Jsoup;
 import org.jsoup.nodes.Document;
 import org.jsoup.nodes.Element;
 
-public class NodesHtmlGenerator {
+public abstract class NodesHtmlGenerator {
 
     private final Node parentNode;
-    private final User user;
-    private final String authority;
 
-    private Element tbody;
+    protected final User user;
+    protected final String authority;
 
-    public NodesHtmlGenerator(Node node, User user, String authority) {
+    private Element containerElement;
+
+    protected NodesHtmlGenerator(Node node, User user, String authority) {
         this.parentNode = node;
         this.user = user;
         this.authority = authority;
@@ -34,143 +31,30 @@ public class NodesHtmlGenerator {
 
         Document html = Jsoup.parse("<html></html>");
 
-        tbody = html.body().appendElement("tbody");
-        tbody.attr("id", "nodes");
-
+        containerElement = createContainerElement(html);
+        
         if (parentNode instanceof ContainerNode) {
             ContainerNode folder = (ContainerNode) parentNode;
             for (Node child : folder.getNodes()) {
-                addChild(child);
+                addChild(child, containerElement);
             }
         }
 
-        return tbody.toString();
-    }
-
-    private void addChild(Node child) {
-        NodeInfo nodeInfo = new NodeInfo(child, user, authority);
-
-        if (nodeInfo.isListOfFiles()) {
-            // hidden file
-            return;
-        }
-
-        Element row = tbody.appendElement("tr");
-
-        addSelectionCell(nodeInfo, row);
-        addLinkCell(nodeInfo, row);
-        addSizeCell(nodeInfo, row);
-        addGroupReadCell(nodeInfo, row);
-        addGroupWriteCell(nodeInfo, row);
-        addActionsCell(nodeInfo, row);
-    }
-
-    private void addSelectionCell(NodeInfo nodeInfo, Element row) {
-
-        Element cell = row.appendElement("td");
-
-        Element input = cell.appendElement("input");
-        input.attr("type", "checkbox");
-        input.attr("data-node", nodeInfo.getPath());
-
-        if (nodeInfo.isAsyncTrans()) {
-            input.addClass("async");
-        } else if (nodeInfo.isDeletable()) {
-            input.addClass("deletable");
-        }
+        return containerElement.toString();
     }
 
-    private void addLinkCell(NodeInfo nodeInfo, Element row) {
+    protected abstract Element createContainerElement(Document doc);
+    
+    protected abstract void addChild(Node child, Element containerElement);
 
-        Element cell = row.appendElement("td");
-        addNodeIcon(nodeInfo, cell);
-        addLink(nodeInfo, cell);
-    }
-
-    private void addSizeCell(NodeInfo nodeInfo, Element row) {
-        Element cell = row.appendElement("td");
-        cell.text(nodeInfo.getSize());
-    }
-
-    private void addGroupReadCell(NodeInfo nodeInfo, Element row) {
-        Element cell = row.appendElement("td");
-        fillGroupCell(cell, nodeInfo.getGroupRead());
-    }
-
-    private void addGroupWriteCell(NodeInfo nodeInfo, Element row) {
-        Element cell = row.appendElement("td");
-        fillGroupCell(cell, nodeInfo.getGroupWrite());
-    }
-
-    private void fillGroupCell(Element cell, String groups) {
-        String[] values = groups.split(" ");
-        List<String> personGroups = new ArrayList<>();
-        List<String> peopleGroups = new ArrayList<>();
-        for (String value : values) {
-            if (!value.isBlank()) {
-                if (value.startsWith("people.")) {
-                    personGroups.add(value.substring("people.".length()).replace("\\.", "."));
-                } else {
-                    peopleGroups.add(value);
-                }
-            }
-        }
-        if (!personGroups.isEmpty()) {
-            Element personIcon = cell.appendElement("span");
-            personIcon.attr("class", "icon person-icon");
-            cell.appendText(String.join(" ", personGroups));
-            cell.append("&nbsp;");
-        }
-        if (!peopleGroups.isEmpty()) {
-            Element personIcon = cell.appendElement("span");
-            personIcon.attr("class", "icon people-icon");
-            cell.appendText(String.join(" ", peopleGroups));
-        }
-    }
-
-    private void addActionsCell(NodeInfo nodeInfo, Element row) {
-        Element cell = row.appendElement("td");
-
-        Element dotsMenu = cell.appendElement("span");
-        dotsMenu.attr("class", "dots-menu icon dots-menu-icon pointer");
-
-        Element dropdown = dotsMenu.appendElement("span");
-        dropdown.attr("class", "dots-menu-content dropdown-menu");
-
-        String nodePathJs = makeJsArg(nodeInfo.getPath());
-
-        if (nodeInfo.isWritable()) {
-            Element shareBtn = dropdown.appendElement("button");
-            shareBtn.text("Share");
-            shareBtn.attr("type", "button");
-            shareBtn.attr("class", "dropdown-item");
-            shareBtn.attr("onclick", "shareNode(" + nodePathJs
-                    + "," + makeJsArg(nodeInfo.getGroupRead())
-                    + "," + makeJsArg(nodeInfo.getGroupWrite()) + ")");
-        }
-        if (nodeInfo.isDeletable()) {
-            Element renameBtn = dropdown.appendElement("button");
-            renameBtn.text("Rename");
-            renameBtn.attr("type", "button");
-            renameBtn.attr("class", "dropdown-item");
-            renameBtn.attr("onclick", "renameNode(" + nodePathJs + ")");
-
-            Element deleteBtn = dropdown.appendElement("button");
-            deleteBtn.text("Delete");
-            deleteBtn.attr("type", "button");
-            deleteBtn.attr("class", "dropdown-item");
-            deleteBtn.attr("onclick", "deleteNode(" + nodePathJs + ")");
-        }
-    }
-
-    private String makeJsArg(String arg) {
+    protected String makeJsArg(String arg) {
         return "'" + arg.replace("\\", "\\\\").replace("'", "\\'") + "'";
     }
 
-    private void addNodeIcon(NodeInfo nodeInfo, Element cell) {
+    protected void addNodeIcon(NodeInfo nodeInfo, Element parentElement) {
 
         if (nodeInfo.isBusy()) {
-            Element loadingWrapper = cell.appendElement("span");
+            Element loadingWrapper = parentElement.appendElement("span");
             loadingWrapper.addClass("node-busy");
             Element spinner = loadingWrapper.appendElement("span");
             spinner.attr("role", "status");
@@ -180,7 +64,7 @@ public class NodesHtmlGenerator {
             srEl.text("Loading...");
         }
 
-        Element icon = cell.appendElement("span");
+        Element icon = parentElement.appendElement("span");
         icon.addClass("icon");
 
         if (nodeInfo.isFolder()) {
@@ -197,49 +81,6 @@ public class NodesHtmlGenerator {
             }
         }
 
-        cell.append("&nbsp;");
-    }
-
-    private void addLink(NodeInfo nodeInfo, Element cell) {
-        if (isDownloadable(nodeInfo, user)) {
-            Element link = cell.appendElement("a");
-            String href;
-            if (nodeInfo.isFolder()) {
-                href = "#/nodes" + urlEncodePath(nodeInfo.getPath());
-            } else {
-                href = "download" + urlEncodePath(nodeInfo.getPath());
-                link.attr("target", "blank_");
-            }
-            link.attr("href", href);
-            link.text(nodeInfo.getName());
-        } else {
-            cell.appendText(nodeInfo.getName());
-        }
-    }
-
-    private boolean isDownloadable(NodeInfo nodeInfo, User user) {
-        if (nodeInfo.isFile()) {
-            if (nodeInfo.isAsyncTrans() || nodeInfo.isBusy()) {
-                return false;
-            }
-        }
-        if (nodeInfo.isPublic()) {
-            return true;
-        }
-
-        if (nodeInfo.getCreator().equals(user.getName())) {
-            return true;
-        }
-
-        if (user.getGroups() != null && !user.getGroups().isEmpty() && !nodeInfo.getGroupRead().isEmpty()) {
-            List<String> groupRead = Arrays.asList(nodeInfo.getGroupRead().split(" "));
-            for (String group : groupRead) {
-                if (user.getGroups().contains(group)) {
-                    return true;
-                }
-            }
-        }
-
-        return false;
+        parentElement.append("&nbsp;");
     }
 }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
index 7fb01941ad5b7c86f7ff39375293f9a61cf76782..75571a3372d7dfc868a54e7a08bdb58d2638cf94 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/controller/NodesControllerTest.java
@@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
 import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
 import java.util.Arrays;
 import java.util.List;
+import net.ivoa.xml.vospace.v2.ContainerNode;
 import net.ivoa.xml.vospace.v2.DataNode;
+import static org.junit.jupiter.api.Assertions.assertFalse;
 import static org.junit.jupiter.api.Assertions.assertTrue;
 import org.junit.jupiter.api.Test;
 import static org.mockito.ArgumentMatchers.any;
@@ -106,4 +108,35 @@ public class NodesControllerTest {
 
         assertTrue(exception);
     }
+
+    @Test
+    public void testListNodesForMoveModal() throws Exception {
+
+        ContainerNode parent = new ContainerNode();
+        parent.setUri("vos://example.com!vospace/a/b/c");
+
+        ContainerNode child1 = new ContainerNode();
+        child1.setUri("vos://example.com!vospace/a/b/c/c1");
+        parent.getNodes().add(child1);
+
+        DataNode child2 = new DataNode();
+        child2.setUri("vos://example.com!vospace/a/b/c/c2");
+        parent.getNodes().add(child2);
+
+        ContainerNode child3 = new ContainerNode();
+        child3.setUri("vos://example.com!vospace/a/b/c/c3");
+        parent.getNodes().add(child3);
+
+        when(client.getNode(any())).thenReturn(parent);
+
+        String response = mockMvc.perform(get("/nodesForMove?path=/a/b/c&nodeToMove=/a/b/c/c3"))
+                .andExpect(status().isOk())
+                .andReturn().getResponse().getContentAsString();
+
+        assertTrue(response.contains("c1")); // folder
+        assertFalse(response.contains("c2")); // data node
+        assertFalse(response.contains("c3")); // nodeToMove
+
+        verify(client, times(1)).getNode(eq("/a/b/c"));
+    }
 }
diff --git a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
index 31f0f207b3ddc601e2639a38aee0518d8e61060e..2e6a1af5d3396dcd16d26d510f5890ffbf274f4e 100644
--- a/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
+++ b/vospace-ui-backend/src/test/java/it/inaf/ia2/vospace/ui/service/NodesHtmlGeneratorTest.java
@@ -33,7 +33,7 @@ public class NodesHtmlGeneratorTest {
         user.setUserId("user_id");
         user.setGroups(Arrays.asList("group1", "group2"));
 
-        NodesHtmlGenerator generator = new NodesHtmlGenerator(parent, user, "example.com!vospace");
+        MainNodesHtmlGenerator generator = new MainNodesHtmlGenerator(parent, user, "example.com!vospace");
 
         String html = generator.generateNodes();
 
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
index 2527b190cc2166c84b163caf6d1cebfe630fc7fd..b227fd46f2ca9f57eb16a8078f296d93d8ab6bac 100644
--- a/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder1.json
@@ -1,4 +1,4 @@
 {
   "writable": true,
-  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2\" /></td>    <td>      <span class=\"icon folder-x-icon\"></span>      <a href=\"#/nodes/folder1/folder2\">folder2</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1/file2\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file2\">file2</a>    </td>    <td>30 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/file3\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file3    </td>    <td>12 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
+  "html": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2\" /></td>    <td>      <span class=\"icon folder-x-icon\"></span>      <a href=\"#/nodes/folder1/folder2\">folder2</a>    </td>    <td>0 B</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/folder1/file2\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file2\">file2</a>    </td>    <td>30 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/file3\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file3    </td>    <td>12 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
 }
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
index be52e8203e87de9972b10dabfea1050d340837c6..904a8a1f38ddf639b744de6a8f0997a9273b3a73 100644
--- a/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/folder2.json
@@ -1,4 +1,4 @@
 {
   "writable": false,
-  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file4\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file4    </td>    <td>10 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file5\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file5    </td>    <td>15 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
+  "html": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file4\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file4    </td>    <td>10 KB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr>  <tr>    <td><input type=\"checkbox\" class=\"async\" data-node=\"/folder1/folder2/file5\" /></td>    <td>      <span class=\"icon file-x-icon\"></span>      file5    </td>    <td>15 MB</td>    <td>group3</td>    <td>group4</td>    <td></td>  </tr></tbody>"
 }
diff --git a/vospace-ui-frontend/src/api/mock/data/nodes/root.json b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
index e0e2b8c82128e7311912dcc9f3bc5ee00d9e5406..d568a9cad5eeaa8e6ddcca475b4f6c259c34594a 100644
--- a/vospace-ui-frontend/src/api/mock/data/nodes/root.json
+++ b/vospace-ui-frontend/src/api/mock/data/nodes/root.json
@@ -1,4 +1,4 @@
 {
   "writable": true,
-  "htmlTable": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"deletable\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td data-groupread=\"[\"people.group1\",\"group2\"]\"><span class=\"icon person-icon\"></span>group1 <span class=\"icon people-icon\"></span>group2</td>    <td>group2</td>    <td>      <span class=\"icon share-icon pointer\" onclick=\"shareNode('/folder1','group1','people.group2')\"></span> &nbsp; <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
+  "html": "<tbody id=\"nodes\">  <tr>    <td><input type=\"checkbox\" class=\"deletable\" data-node=\"/folder1\" /></td>    <td>      <span class=\"icon folder-icon\"></span>      <a href=\"#/nodes/folder1\">folder1</a>    </td>    <td>0 B</td>    <td data-groupread=\"[\"people.group1\",\"group2\"]\"><span class=\"icon person-icon\"></span>group1 <span class=\"icon people-icon\"></span>group2</td>    <td>group2</td>    <td>      <span class=\"icon share-icon pointer\" onclick=\"shareNode('/folder1','group1','people.group2')\"></span> &nbsp; <span class=\"icon trash-icon pointer\" onclick=\"deleteNode('/folder1')\"></span>    </td>  </tr>  <tr>    <td><input type=\"checkbox\" data-node=\"/file1\" /></td>    <td>      <span class=\"icon file-icon\"></span>      <a href=\"download/file1\">file1</a>    </td>    <td>12 MB</td>    <td>group1</td>    <td>group2</td>    <td></td>  </tr></tbody>"
 }
diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js
index 5d9284030f262970c22f766aeda44f42d0425dcf..bdf969c48782838ffc69eeed150486f49b358ea4 100644
--- a/vospace-ui-frontend/src/api/server/index.js
+++ b/vospace-ui-frontend/src/api/server/index.js
@@ -181,6 +181,17 @@ export default {
       data
     }, true, true);
   },
+  getNodesForMove(data) {
+    let url = BASE_API_URL + 'nodesForMove?path=' + escapePath(data.path) + '&nodeToMove=' + data.nodeToMove;
+    return apiRequest({
+      method: 'GET',
+      url: url,
+      withCredentials: true,
+      headers: {
+        'Cache-Control': 'no-cache'
+      }
+    }, true, true);
+  },
   moveNode(data) {
     let url = BASE_API_URL + 'move';
     return apiRequest({
diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue
index c2be3ae3bb5ce2f38cdd1c0947bf5d5f93dfd220..ac26e0a4115a6deab1faf5d2f117fd205ae60668 100644
--- a/vospace-ui-frontend/src/components/Main.vue
+++ b/vospace-ui-frontend/src/components/Main.vue
@@ -40,6 +40,7 @@
   <ConfirmDeleteModal />
   <ShareModal />
   <RenameModal />
+  <MoveModal />
 </div>
 </template>
 
@@ -50,6 +51,7 @@ import UploadFilesModal from './modal/UploadFilesModal.vue'
 import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue'
 import ShareModal from './modal/ShareModal.vue'
 import RenameModal from './modal/RenameModal.vue'
+import MoveModal from './modal/MoveModal.vue'
 
 export default {
   components: {
@@ -59,7 +61,8 @@ export default {
     UploadFilesModal,
     ConfirmDeleteModal,
     ShareModal,
-    RenameModal
+    RenameModal,
+    MoveModal
   },
   computed: {
     breadcrumbs() {
diff --git a/vospace-ui-frontend/src/components/modal/MoveModal.vue b/vospace-ui-frontend/src/components/modal/MoveModal.vue
new file mode 100644
index 0000000000000000000000000000000000000000..53a3a88aab438138386a1a9bfc4f1e88b40eddf6
--- /dev/null
+++ b/vospace-ui-frontend/src/components/modal/MoveModal.vue
@@ -0,0 +1,79 @@
+<!--
+  This file is part of vospace-ui
+  Copyright (C) 2021 Istituto Nazionale di Astrofisica
+  SPDX-License-Identifier: GPL-3.0-or-later
+-->
+<template>
+<b-modal id="move-modal" :title="'Move ' + nodeToMove + ' to ' + destinationPath" okTitle="Move node" @show="afterShow" @ok.prevent="moveNode" :ok-disabled="!writable" size="lg">
+  <ol class="breadcrumb">
+    <li class="breadcrumb-item" v-for="(item, i) in breadcrumbs" :key="i" :class="{ 'active' : item.active }">
+      <a href="#" @click.stop.prevent="breadcrumbClick(i)" v-if="!item.active">{{item.text}}</a>
+      <span v-if="item.active">{{item.text}}</span>
+    </li>
+  </ol>
+  <div id="move-nodes-wrapper">
+    <div id="move-nodes"></div>
+  </div>
+</b-modal>
+</template>
+
+<script>
+export default {
+  name: 'MoveModal',
+  computed: {
+    nodeToMove() { return this.$store.state.nodeToMove },
+    destinationPath() { return this.$store.state.nodeToMoveDestination },
+    writable() { return this.$store.state.nodeToMoveDestinationWritable },
+    breadcrumbs() {
+      let items = [];
+      if (this.destinationPath !== null) {
+        let pathSplit = this.destinationPath.split('/');
+        for (let i = 0; i < pathSplit.length; i++) {
+          items.push({
+            text: i === 0 ? 'ROOT' : pathSplit[i],
+            active: i === pathSplit.length - 1
+          });
+        }
+      }
+      return items;
+    }
+  },
+  methods: {
+    afterShow() {
+      // starts from parent path
+      this.$store.dispatch('openNodeInMoveModal', this.nodeToMove.substring(0, this.nodeToMove.lastIndexOf('/')));
+    },
+    breadcrumbClick(i) {
+      let pathSplit = this.destinationPath.split('/');
+      let path = pathSplit.slice(0, i + 1).map(p => encodeURIComponent(p)).join('/');
+      if (path === '') {
+        path = '/';
+      }
+      this.$store.commit('setNodeToMoveDestination', path);
+      window.openNodeInMoveModal(event, path);
+    },
+    moveNode() {
+      this.$store.dispatch('moveNode', {
+          target: this.nodeToMove,
+          direction: this.destinationPath
+        })
+        .then(() => {
+          this.$bvModal.hide('move-modal');
+        })
+    }
+  }
+}
+</script>
+
+<style>
+#move-nodes .list-group-item {
+  /* reduced padding */
+  padding-top: .35rem;
+  padding-bottom: .35rem;
+}
+
+#move-nodes-wrapper {
+  max-height: 300px;
+  overflow-y: auto;
+}
+</style>
diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js
index d1caac178c7a6d168d56bef1ed36cfab97495f9b..7ba1ed9e287560485e5bfa585552f2a73a2dd71d 100644
--- a/vospace-ui-frontend/src/main.js
+++ b/vospace-ui-frontend/src/main.js
@@ -35,6 +35,15 @@ window.renameNode = function(path) {
   store.commit('setNodeToRename', path);
   vm.$bvModal.show('rename-modal');
 }
+window.moveNode = function(path) {
+  store.commit('setNodeToMove', path);
+  vm.$bvModal.show('move-modal');
+}
+window.openNodeInMoveModal = function(event, path) {
+  event.preventDefault();
+  event.stopPropagation();
+  store.dispatch('openNodeInMoveModal', path);
+}
 
 export default {
   showError(message) {
diff --git a/vospace-ui-frontend/src/nodesReloader.js b/vospace-ui-frontend/src/nodesReloader.js
index 2926ef9ba5ed04d508bebeffe5234473d0d738d0..a2a04cb4f1fd895a1223937e30c0898e11be4609 100644
--- a/vospace-ui-frontend/src/nodesReloader.js
+++ b/vospace-ui-frontend/src/nodesReloader.js
@@ -34,7 +34,7 @@ function checkNodes() {
       .then(res => {
         // check that path didn't change in meantime by user action
         if (path === store.state.path) {
-          let resHasBusyNodes = res.htmlTable.includes('node-busy');
+          let resHasBusyNodes = res.html.includes('node-busy');
           if ((!busyNodes && resHasBusyNodes) || (busyNodes && !resHasBusyNodes)) {
             store.dispatch('setNodes', res);
           } else {
diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js
index d10ef32385e3a008f64e289a856ee7b0653feca1..f61753ddcf702e521a909279ec398da2e0b79fb2 100644
--- a/vospace-ui-frontend/src/store.js
+++ b/vospace-ui-frontend/src/store.js
@@ -39,7 +39,10 @@ export default new Vuex.Store({
       groupRead: null,
       groupWrite: null
     },
-    nodeToRename: null
+    nodeToRename: null,
+    nodeToMove: null,
+    nodeToMoveDestination: null,
+    nodeToMoveDestinationWritable: false
   },
   mutations: {
     setLoading(state, loading) {
@@ -85,6 +88,15 @@ export default new Vuex.Store({
     },
     setNodeToRename(state, path) {
       state.nodeToRename = path;
+    },
+    setNodeToMove(state, path) {
+      state.nodeToMove = path;
+    },
+    setNodeToMoveDestination(state, path) {
+      state.nodeToMoveDestination = path;
+    },
+    setNodeToMoveDestinationWritable(state, value) {
+      state.nodeToMoveDestinationWritable = value;
     }
   },
   actions: {
@@ -99,7 +111,7 @@ export default new Vuex.Store({
     },
     setNodes({ commit, dispatch }, res) {
       commit('setWritable', res.writable);
-      document.getElementById('nodes').outerHTML = res.htmlTable;
+      document.getElementById('nodes').outerHTML = res.html;
       let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]');
       for (let i = 0; i < checkboxes.length; i++) {
         checkboxes[i].addEventListener('change', function() {
@@ -201,6 +213,14 @@ export default new Vuex.Store({
           // Reload current node
           dispatch('setPath', state.path);
         });
+    },
+    openNodeInMoveModal({ state, commit }, path) {
+      commit('setNodeToMoveDestination', path);
+      client.getNodesForMove({ path, nodeToMove: state.nodeToMove })
+        .then(res => {
+          commit('setNodeToMoveDestinationWritable', res.writable);
+          document.getElementById('move-nodes').outerHTML = res.html;
+        });
     }
   }
 });