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

Replaced action buttons with dots menu. Added modal for renaming files (WIP)

parent 93879b16
No related branches found
No related tags found
No related merge requests found
Pipeline #1946 passed
......@@ -24,7 +24,6 @@ import java.net.http.HttpResponse;
import java.net.http.HttpResponse.BodyHandlers;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Scanner;
import java.util.concurrent.CompletionException;
import java.util.concurrent.ForkJoinPool;
......
......@@ -16,6 +16,7 @@ import java.util.Optional;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
import javax.servlet.http.HttpServletRequest;
import net.ivoa.xml.uws.v1.JobSummary;
import net.ivoa.xml.vospace.v2.ContainerNode;
import net.ivoa.xml.vospace.v2.Node;
import net.ivoa.xml.vospace.v2.Property;
......@@ -134,6 +135,21 @@ public class NodesController extends BaseController {
return ResponseEntity.noContent().build();
}
@PostMapping(value = "/move")
public void moveNode(@RequestBody Map<String, Object> params) {
String target = getRequiredParam(params, "target");
String direction = getRequiredParam(params, "direction");
Transfer transfer = new Transfer();
transfer.setTarget(target);
transfer.setDirection(direction);
JobSummary job = client.startTransferJob(transfer);
// TODO: polling
}
protected String getPath(String prefix) {
String requestURL = servletRequest.getRequestURL().toString();
return NodeUtils.getPathFromRequestURLString(requestURL, prefix);
......
......@@ -130,18 +130,36 @@ public class NodesHtmlGenerator {
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 shareIcon = cell.appendElement("span");
shareIcon.attr("class", "icon share-icon pointer");
shareIcon.attr("onclick", "shareNode(" + makeJsArg(nodeInfo.getPath())
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()) {
cell.append("&nbsp;");
Element deleteIcon = cell.appendElement("span");
deleteIcon.attr("class", "icon trash-icon pointer");
deleteIcon.attr("onclick", "deleteNode(" + makeJsArg(nodeInfo.getPath()) + ")");
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 + ")");
}
}
......
......@@ -47,6 +47,19 @@ export default {
self.$store.dispatch('checkJobs');
}
}, 1000);
// Add event listener for dots menus
document.addEventListener('click', function(event) {
if (event.target.classList.contains('dots-menu')) {
event.target.classList.toggle('active')
} else {
if (event.target.closest('.dots-menu') === null) {
for (let menu of document.querySelectorAll('.dots-menu')) {
menu.classList.remove('active');
}
}
}
});
}
}
</script>
......@@ -111,4 +124,22 @@ export default {
.node-busy+.icon {
margin-right: 3px;
}
.dots-menu {
position: relative;
}
.dots-menu-content {
display: none;
position: absolute;
/*background-color: #f9f9f9;
min-width: 160px;
box-shadow: 0px 8px 16px 0px rgba(0,0,0,0.2);
padding: 12px 16px;
z-index: 1;*/
}
.dots-menu.active .dots-menu-content {
display: block;
}
</style>
......@@ -180,5 +180,17 @@ export default {
},
data
}, true, true);
},
moveNode(data) {
let url = BASE_API_URL + 'move';
return apiRequest({
method: 'POST',
url: url,
withCredentials: true,
headers: {
'Cache-Control': 'no-cache'
},
data
}, true, true);
}
}
......@@ -44,3 +44,7 @@
.person-icon {
background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='person fill' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-person-fill mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M3 14s-1 0-1-1 1-4 6-4 6 3 6 4-1 1-1 1H3zm5-6a3 3 0 1 0 0-6 3 3 0 0 0 0 6z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
.dots-menu-icon {
background-image: url("data:image/svg+xml,%3Csvg data-v-41be6633='' viewBox='0 0 16 16' width='1em' height='1em' focusable='false' role='img' aria-label='three dots vertical' xmlns='http://www.w3.org/2000/svg' fill='currentColor' class='bi-three-dots-vertical mx-auto b-icon bi'%3E%3Cg data-v-41be6633=''%3E%3Cpath d='M9.5 13a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0zm0-5a1.5 1.5 0 1 1-3 0 1.5 1.5 0 0 1 3 0z'%3E%3C/path%3E%3C/g%3E%3C/svg%3E");
}
......@@ -39,6 +39,7 @@
<UploadFilesModal />
<ConfirmDeleteModal />
<ShareModal />
<RenameModal />
</div>
</template>
......@@ -48,6 +49,7 @@ import CreateFolderModal from './modal/CreateFolderModal.vue'
import UploadFilesModal from './modal/UploadFilesModal.vue'
import ConfirmDeleteModal from './modal/ConfirmDeleteModal.vue'
import ShareModal from './modal/ShareModal.vue'
import RenameModal from './modal/RenameModal.vue'
export default {
components: {
......@@ -56,7 +58,8 @@ export default {
CreateFolderModal,
UploadFilesModal,
ConfirmDeleteModal,
ShareModal
ShareModal,
RenameModal
},
computed: {
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="rename-modal" :title="'Rename ' + nodeToRename" okTitle="Rename" @show="reset" @shown="afterShow" @ok.prevent="renameNode">
<b-form inline>
<label class="w-25" for="new-name-input">New name</label>
<b-form-input v-model.trim="newName" id="new-name-input" ref="newNameInput" class="w-75" aria-describedby="new-name-input-feedback" :state="newNameState" v-on:input="resetError" @keydown.native.enter="renameNode">
</b-form-input>
<b-form-invalid-feedback id="new-folder-name-input-feedback" class="text-right">{{newNameError}}</b-form-invalid-feedback>
</b-form>
</b-modal>
</template>
<script>
export default {
name: 'RenameModal',
computed: {
nodeToRename() { return this.$store.state.nodeToRename },
oldName() { return this.nodeToRename.substring(this.nodeToRename.lastIndexOf("/") + 1) },
newNameState() {
if (this.newNameError) {
return false;
}
return null;
}
},
data() {
return {
newName: this.oldName,
newNameError: null
}
},
methods: {
afterShow: function() {
this.$refs.newNameInput.focus();
},
reset() {
this.newName = this.oldName;
this.resetError();
},
resetError() {
this.newNameError = null;
},
renameNode() {
if (this.newName === this.oldName) {
return;
}
if (!this.newName) {
this.newNameError = "Name is required";
} else if (/[<>?":\\/`|'*]/.test(this.newName)) {
this.newNameError = "Name contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `";
} else {
let parentPath = this.oldName.substring(0, this.oldName.lastIndexOf('/') + 1);
this.$store.dispatch('moveNode', {
target: parentPath + this.oldName,
direction: parentPath + this.newName
})
.then(() => {
this.$bvModal.hide('rename-modal');
})
.catch(res => {
this.newFolderNameError = res.message;
});
}
}
}
}
</script>
......@@ -31,6 +31,10 @@ window.shareNode = function(path, groupRead, groupWrite) {
store.commit('setNodeToShare', { path, groupRead, groupWrite });
vm.$bvModal.show('share-modal');
}
window.renameNode = function(path) {
store.commit('setNodeToRename', path);
vm.$bvModal.show('rename-modal');
}
export default {
showError(message) {
......
......@@ -38,7 +38,8 @@ export default new Vuex.Store({
path: null,
groupRead: null,
groupWrite: null
}
},
nodeToRename: null
},
mutations: {
setLoading(state, loading) {
......@@ -81,6 +82,9 @@ export default new Vuex.Store({
state.nodeToShare.path = data.path;
state.nodeToShare.groupRead = data.groupRead;
state.nodeToShare.groupWrite = data.groupWrite;
},
setNodeToRename(state, path) {
state.nodeToRename = path;
}
},
actions: {
......@@ -190,6 +194,13 @@ export default new Vuex.Store({
// Reload current node
dispatch('setPath', state.path);
});
},
moveNode({ state, dispatch }, data) {
client.moveNode(data)
.then(() => {
// Reload current node
dispatch('setPath', state.path);
});
}
}
});
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Please register or to comment