Skip to content
Snippets Groups Projects
Commit f2417395 authored by Sonia Zorba's avatar Sonia Zorba
Browse files

Added modal for moving nodes

parent 336569fc
No related branches found
No related tags found
No related merge requests found
Pipeline #1966 passed
Showing
with 452 additions and 193 deletions
...@@ -8,7 +8,8 @@ package it.inaf.ia2.vospace.ui.controller; ...@@ -8,7 +8,8 @@ package it.inaf.ia2.vospace.ui.controller;
import it.inaf.ia2.aa.data.User; import it.inaf.ia2.aa.data.User;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import it.inaf.ia2.vospace.ui.data.ListNodeData; 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 it.inaf.oats.vospace.datamodel.NodeUtils;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
...@@ -33,6 +34,7 @@ import org.springframework.http.ResponseEntity; ...@@ -33,6 +34,7 @@ import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
...@@ -61,8 +63,23 @@ public class NodesController extends BaseController { ...@@ -61,8 +63,23 @@ public class NodesController extends BaseController {
listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups())); listNodeData.setWritable(NodeUtils.checkIfWritable(node, principal.getName(), principal.getGroups()));
NodesHtmlGenerator htmlGenerator = new NodesHtmlGenerator(node, principal, authority); MainNodesHtmlGenerator htmlGenerator = new MainNodesHtmlGenerator(node, principal, authority);
listNodeData.setHtmlTable(htmlGenerator.generateNodes()); 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); return ResponseEntity.ok(listNodeData);
} }
......
...@@ -7,15 +7,15 @@ package it.inaf.ia2.vospace.ui.data; ...@@ -7,15 +7,15 @@ package it.inaf.ia2.vospace.ui.data;
public class ListNodeData { public class ListNodeData {
private String htmlTable; private String html;
private boolean writable; private boolean writable;
public String getHtmlTable() { public String getHtml() {
return htmlTable; return html;
} }
public void setHtmlTable(String htmlTable) { public void setHtml(String html) {
this.htmlTable = htmlTable; this.html = html;
} }
public boolean isWritable() { public boolean isWritable() {
......
/*
* 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;
}
}
/*
* 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());
}
}
...@@ -6,25 +6,22 @@ ...@@ -6,25 +6,22 @@
package it.inaf.ia2.vospace.ui.service; package it.inaf.ia2.vospace.ui.service;
import it.inaf.ia2.aa.data.User; 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.ContainerNode;
import net.ivoa.xml.vospace.v2.Node; import net.ivoa.xml.vospace.v2.Node;
import org.jsoup.Jsoup; import org.jsoup.Jsoup;
import org.jsoup.nodes.Document; import org.jsoup.nodes.Document;
import org.jsoup.nodes.Element; import org.jsoup.nodes.Element;
public class NodesHtmlGenerator { public abstract class NodesHtmlGenerator {
private final Node parentNode; 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.parentNode = node;
this.user = user; this.user = user;
this.authority = authority; this.authority = authority;
...@@ -34,143 +31,30 @@ public class NodesHtmlGenerator { ...@@ -34,143 +31,30 @@ public class NodesHtmlGenerator {
Document html = Jsoup.parse("<html></html>"); Document html = Jsoup.parse("<html></html>");
tbody = html.body().appendElement("tbody"); containerElement = createContainerElement(html);
tbody.attr("id", "nodes");
if (parentNode instanceof ContainerNode) { if (parentNode instanceof ContainerNode) {
ContainerNode folder = (ContainerNode) parentNode; ContainerNode folder = (ContainerNode) parentNode;
for (Node child : folder.getNodes()) { 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");
}
}
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) { return containerElement.toString();
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"); protected abstract Element createContainerElement(Document doc);
dotsMenu.attr("class", "dots-menu icon dots-menu-icon pointer");
Element dropdown = dotsMenu.appendElement("span"); protected abstract void addChild(Node child, Element containerElement);
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("'", "\\'") + "'"; return "'" + arg.replace("\\", "\\\\").replace("'", "\\'") + "'";
} }
private void addNodeIcon(NodeInfo nodeInfo, Element cell) { protected void addNodeIcon(NodeInfo nodeInfo, Element parentElement) {
if (nodeInfo.isBusy()) { if (nodeInfo.isBusy()) {
Element loadingWrapper = cell.appendElement("span"); Element loadingWrapper = parentElement.appendElement("span");
loadingWrapper.addClass("node-busy"); loadingWrapper.addClass("node-busy");
Element spinner = loadingWrapper.appendElement("span"); Element spinner = loadingWrapper.appendElement("span");
spinner.attr("role", "status"); spinner.attr("role", "status");
...@@ -180,7 +64,7 @@ public class NodesHtmlGenerator { ...@@ -180,7 +64,7 @@ public class NodesHtmlGenerator {
srEl.text("Loading..."); srEl.text("Loading...");
} }
Element icon = cell.appendElement("span"); Element icon = parentElement.appendElement("span");
icon.addClass("icon"); icon.addClass("icon");
if (nodeInfo.isFolder()) { if (nodeInfo.isFolder()) {
...@@ -197,49 +81,6 @@ public class NodesHtmlGenerator { ...@@ -197,49 +81,6 @@ public class NodesHtmlGenerator {
} }
} }
cell.append("&nbsp;"); parentElement.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;
} }
} }
...@@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.ObjectMapper; ...@@ -9,7 +9,9 @@ import com.fasterxml.jackson.databind.ObjectMapper;
import it.inaf.ia2.vospace.ui.client.VOSpaceClient; import it.inaf.ia2.vospace.ui.client.VOSpaceClient;
import java.util.Arrays; import java.util.Arrays;
import java.util.List; import java.util.List;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.DataNode; import net.ivoa.xml.vospace.v2.DataNode;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.any;
...@@ -106,4 +108,35 @@ public class NodesControllerTest { ...@@ -106,4 +108,35 @@ public class NodesControllerTest {
assertTrue(exception); 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"));
}
} }
...@@ -33,7 +33,7 @@ public class NodesHtmlGeneratorTest { ...@@ -33,7 +33,7 @@ public class NodesHtmlGeneratorTest {
user.setUserId("user_id"); user.setUserId("user_id");
user.setGroups(Arrays.asList("group1", "group2")); 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(); String html = generator.generateNodes();
......
{ {
"writable": true, "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>"
} }
{ {
"writable": false, "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>"
} }
{ {
"writable": true, "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>"
} }
...@@ -181,6 +181,17 @@ export default { ...@@ -181,6 +181,17 @@ export default {
data data
}, true, true); }, 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) { moveNode(data) {
let url = BASE_API_URL + 'move'; let url = BASE_API_URL + 'move';
return apiRequest({ return apiRequest({
......
...@@ -40,6 +40,7 @@ ...@@ -40,6 +40,7 @@
<ConfirmDeleteModal /> <ConfirmDeleteModal />
<ShareModal /> <ShareModal />
<RenameModal /> <RenameModal />
<MoveModal />
</div> </div>
</template> </template>
...@@ -50,6 +51,7 @@ import UploadFilesModal from './modal/UploadFilesModal.vue' ...@@ -50,6 +51,7 @@ import UploadFilesModal from './modal/UploadFilesModal.vue'
import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue' import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue'
import ShareModal from './modal/ShareModal.vue' import ShareModal from './modal/ShareModal.vue'
import RenameModal from './modal/RenameModal.vue' import RenameModal from './modal/RenameModal.vue'
import MoveModal from './modal/MoveModal.vue'
export default { export default {
components: { components: {
...@@ -59,7 +61,8 @@ export default { ...@@ -59,7 +61,8 @@ export default {
UploadFilesModal, UploadFilesModal,
ConfirmDeleteModal, ConfirmDeleteModal,
ShareModal, ShareModal,
RenameModal RenameModal,
MoveModal
}, },
computed: { computed: {
breadcrumbs() { breadcrumbs() {
......
<!--
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>
...@@ -35,6 +35,15 @@ window.renameNode = function(path) { ...@@ -35,6 +35,15 @@ window.renameNode = function(path) {
store.commit('setNodeToRename', path); store.commit('setNodeToRename', path);
vm.$bvModal.show('rename-modal'); 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 { export default {
showError(message) { showError(message) {
......
...@@ -34,7 +34,7 @@ function checkNodes() { ...@@ -34,7 +34,7 @@ function checkNodes() {
.then(res => { .then(res => {
// check that path didn't change in meantime by user action // check that path didn't change in meantime by user action
if (path === store.state.path) { if (path === store.state.path) {
let resHasBusyNodes = res.htmlTable.includes('node-busy'); let resHasBusyNodes = res.html.includes('node-busy');
if ((!busyNodes && resHasBusyNodes) || (busyNodes && !resHasBusyNodes)) { if ((!busyNodes && resHasBusyNodes) || (busyNodes && !resHasBusyNodes)) {
store.dispatch('setNodes', res); store.dispatch('setNodes', res);
} else { } else {
......
...@@ -39,7 +39,10 @@ export default new Vuex.Store({ ...@@ -39,7 +39,10 @@ export default new Vuex.Store({
groupRead: null, groupRead: null,
groupWrite: null groupWrite: null
}, },
nodeToRename: null nodeToRename: null,
nodeToMove: null,
nodeToMoveDestination: null,
nodeToMoveDestinationWritable: false
}, },
mutations: { mutations: {
setLoading(state, loading) { setLoading(state, loading) {
...@@ -85,6 +88,15 @@ export default new Vuex.Store({ ...@@ -85,6 +88,15 @@ export default new Vuex.Store({
}, },
setNodeToRename(state, path) { setNodeToRename(state, path) {
state.nodeToRename = path; state.nodeToRename = path;
},
setNodeToMove(state, path) {
state.nodeToMove = path;
},
setNodeToMoveDestination(state, path) {
state.nodeToMoveDestination = path;
},
setNodeToMoveDestinationWritable(state, value) {
state.nodeToMoveDestinationWritable = value;
} }
}, },
actions: { actions: {
...@@ -99,7 +111,7 @@ export default new Vuex.Store({ ...@@ -99,7 +111,7 @@ export default new Vuex.Store({
}, },
setNodes({ commit, dispatch }, res) { setNodes({ commit, dispatch }, res) {
commit('setWritable', res.writable); commit('setWritable', res.writable);
document.getElementById('nodes').outerHTML = res.htmlTable; document.getElementById('nodes').outerHTML = res.html;
let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]'); let checkboxes = document.querySelectorAll('#nodes input[type="checkbox"]');
for (let i = 0; i < checkboxes.length; i++) { for (let i = 0; i < checkboxes.length; i++) {
checkboxes[i].addEventListener('change', function() { checkboxes[i].addEventListener('change', function() {
...@@ -201,6 +213,14 @@ export default new Vuex.Store({ ...@@ -201,6 +213,14 @@ export default new Vuex.Store({
// Reload current node // Reload current node
dispatch('setPath', state.path); 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;
});
} }
} }
}); });
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment