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(" "); + } + 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(" "); - } - 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(" "); - } - - 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(" "); } } 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> <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> <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; + }); } } });