From 0341cfd66a99d15c8627de74e7aa80b686aba819 Mon Sep 17 00:00:00 2001 From: Sonia Zorba <sonia.zorba@inaf.it> Date: Mon, 30 Aug 2021 17:27:51 +0200 Subject: [PATCH] UI: added methods for copying nodes and handles copy/move of multiple nodes at the same time --- vospace-ui-frontend/src/api/server/index.js | 9 +- vospace-ui-frontend/src/components/Main.vue | 50 +++++++- .../src/components/modal/MoveModal.vue | 79 ------------- .../src/components/modal/MoveOrCopyModal.vue | 111 ++++++++++++++++++ .../src/components/modal/RenameModal.vue | 7 +- vospace-ui-frontend/src/main.js | 18 ++- vospace-ui-frontend/src/store.js | 65 ++++++---- 7 files changed, 226 insertions(+), 113 deletions(-) delete mode 100644 vospace-ui-frontend/src/components/modal/MoveModal.vue create mode 100644 vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue diff --git a/vospace-ui-frontend/src/api/server/index.js b/vospace-ui-frontend/src/api/server/index.js index dde1066..4d09ab4 100644 --- a/vospace-ui-frontend/src/api/server/index.js +++ b/vospace-ui-frontend/src/api/server/index.js @@ -199,8 +199,9 @@ export default { data }, true, true); }, - getNodesForMove(data) { - let url = BASE_API_URL + 'nodesForMove?path=' + escapePath(data.path) + '&nodeToMove=' + data.nodeToMove; + getNodesForMoveOrCopy(data) { + let url = BASE_API_URL + 'nodesForMoveOrCopy?path=' + escapePath(data.path) + + '&' + data.targets.map(t => 'target=' + escapePath(t)).join('&'); return apiRequest({ method: 'GET', url: url, @@ -210,8 +211,8 @@ export default { } }, true, true); }, - moveNode(data) { - let url = BASE_API_URL + 'move'; + moveOrCopyNodes(data) { + let url = BASE_API_URL + 'moveOrCopy'; return apiRequest({ method: 'POST', url: url, diff --git a/vospace-ui-frontend/src/components/Main.vue b/vospace-ui-frontend/src/components/Main.vue index cb505b8..9649232 100644 --- a/vospace-ui-frontend/src/components/Main.vue +++ b/vospace-ui-frontend/src/components/Main.vue @@ -12,6 +12,8 @@ <b-dropdown variant="primary" text="Actions" v-if="actionsEnabled"> <b-dropdown-item :disabled="!asyncButtonEnabled" @click="startAsyncRecallJob">Async recall</b-dropdown-item> <b-dropdown-item :disabled="!deleteButtonEnabled" @click="deleteNodes">Delete</b-dropdown-item> + <b-dropdown-item :disabled="!moveButtonEnabled" @click="moveNodes">Move</b-dropdown-item> + <b-dropdown-item :disabled="!copyButtonEnabled" @click="copyNodes">Copy</b-dropdown-item> <b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('zip')">Create zip archive</b-dropdown-item> <b-dropdown-item :disabled="!archiveButtonEnabled" @click="createArchive('tar')">Create tar archive</b-dropdown-item> </b-dropdown> @@ -44,7 +46,7 @@ <ConfirmDeleteModal /> <ShareModal /> <RenameModal /> - <MoveModal /> + <MoveOrCopyModal /> <ConfirmArchiveModal /> </div> </template> @@ -56,7 +58,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' +import MoveOrCopyModal from './modal/MoveOrCopyModal.vue' import ConfirmArchiveModal from './modal/ConfirmArchiveModal.vue' export default { @@ -68,7 +70,7 @@ export default { ConfirmDeleteModal, ShareModal, RenameModal, - MoveModal, + MoveOrCopyModal, ConfirmArchiveModal }, computed: { @@ -98,9 +100,15 @@ export default { deleteButtonEnabled() { return this.$store.state.deleteButtonEnabled; }, + moveButtonEnabled() { + return this.$store.state.moveButtonEnabled; + }, archiveButtonEnabled() { return this.$store.state.archiveButtonEnabled; }, + copyButtonEnabled() { + return this.$store.state.copyButtonEnabled; + }, writable() { return this.$store.state.writable; } @@ -147,6 +155,42 @@ export default { this.$store.commit('setSelectedUndeletableNodes', unDeletablePaths); this.$bvModal.show('confirm-delete-modal'); }, + moveNodes() { + let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked'); + let paths = []; + let unDeletablePaths = []; + for (let i = 0; i < selectedNodesCheckboxes.length; i++) { + let checkbox = selectedNodesCheckboxes[i]; + let dataNode = checkbox.getAttribute('data-node'); + if (checkbox.classList.contains('deletable')) { + paths.push(dataNode); + } else { + unDeletablePaths.push(dataNode); + } + } + this.$store.commit('setNodesToMoveOrCopy', paths); + this.$store.commit('setMoveOrCopy', 'move'); + this.$store.commit('setSelectedNotMovableNodes', unDeletablePaths); + this.$bvModal.show('move-or-copy-modal'); + }, + copyNodes() { + let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked'); + let paths = []; + let unCopiablePaths = []; + for (let i = 0; i < selectedNodesCheckboxes.length; i++) { + let checkbox = selectedNodesCheckboxes[i]; + let dataNode = checkbox.getAttribute('data-node'); + if (checkbox.classList.contains('async')) { + unCopiablePaths.push(dataNode); + } else { + paths.push(dataNode); + } + } + this.$store.commit('setNodesToMoveOrCopy', paths); + this.$store.commit('setMoveOrCopy', 'copy'); + this.$store.commit('setSelectedNotCopiableNodes', unCopiablePaths); + this.$bvModal.show('move-or-copy-modal'); + }, createArchive(type) { let selectedNodesCheckboxes = document.querySelectorAll('#nodes input:checked'); let nodesToArchive = []; diff --git a/vospace-ui-frontend/src/components/modal/MoveModal.vue b/vospace-ui-frontend/src/components/modal/MoveModal.vue deleted file mode 100644 index 53a3a88..0000000 --- a/vospace-ui-frontend/src/components/modal/MoveModal.vue +++ /dev/null @@ -1,79 +0,0 @@ -<!-- - 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/components/modal/MoveOrCopyModal.vue b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue new file mode 100644 index 0000000..2534b92 --- /dev/null +++ b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue @@ -0,0 +1,111 @@ +<!-- + 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-or-copy-modal" :title="title" :okTitle="okTitle" @show="afterShow" @ok.prevent="moveOrCopyNodes" :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-or-copy-nodes-wrapper"> + <div id="move-or-copy-nodes"></div> + </div> + <div v-if="selectedNotCopiableNodes.length > 0" class="mt-3"> + <p><strong>Warning</strong>: following selected nodes can't be copied and will be ignored:</p> + <p> + <ul> + <li v-for="node in selectedNotCopiableNodes" :key="node">{{node}}</li> + </ul> + </p> + </div> + <div v-if="selectedNotMovableNodes.length > 0" class="mt-3"> + <p><strong>Warning</strong>: following selected nodes can't be moved and will be ignored:</p> + <p> + <ul> + <li v-for="node in selectedNotMovableNodes" :key="node">{{node}}</li> + </ul> + </p> + </div> +</b-modal> +</template> + +<script> +export default { + name: 'MoveOrCopyModal', + computed: { + nodesToMoveOrCopy() { return this.$store.state.nodesToMoveOrCopy }, + destinationPath() { return this.$store.state.moveOrCopyDestination }, + writable() { return this.$store.state.moveOrCopyDestinationWritable }, + moveOrCopy() { return this.$store.state.moveOrCopy }, + selectedNotMovableNodes() { return this.$store.state.selectedNotMovableNodes }, + selectedNotCopiableNodes() { return this.$store.state.selectedNotCopiableNodes }, + title() { + let text = this.moveOrCopy === 'move' ? 'Move ' : 'Copy '; + if (this.nodesToMoveOrCopy.length === 1) { + text += this.nodesToMoveOrCopy[0]; + } else { + text += this.nodesToMoveOrCopy.length + ' nodes'; + } + text += ' to ' + this.destinationPath; + return text; + }, + okTitle() { return (this.moveOrCopy === 'move' ? 'Move nodes' : 'Copy nodes') }, + 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 + let firstSelectedNode = this.nodesToMoveOrCopy[0]; + this.$store.dispatch('openNodeInMoveOrCopyModal', firstSelectedNode.substring(0, firstSelectedNode.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('setMoveOrCopyDestination', path); + window.openNodeInMoveOrCopyModal(event, path); + }, + moveOrCopyNodes() { + this.$store.dispatch('moveOrCopyNodes', { + targets: this.nodesToMoveOrCopy, + direction: this.destinationPath, + keepBytes: this.moveOrCopy === 'copy' + }) + .then(() => { + this.$bvModal.hide('move-or-copy-modal'); + }) + } + } +} +</script> + +<style> +#move-or-copy-nodes .list-group-item { + /* reduced padding */ + padding-top: .35rem; + padding-bottom: .35rem; +} + +#move-or-copy-nodes-wrapper { + max-height: 300px; + overflow-y: auto; +} +</style> diff --git a/vospace-ui-frontend/src/components/modal/RenameModal.vue b/vospace-ui-frontend/src/components/modal/RenameModal.vue index 54bfc06..4b86e4b 100644 --- a/vospace-ui-frontend/src/components/modal/RenameModal.vue +++ b/vospace-ui-frontend/src/components/modal/RenameModal.vue @@ -54,9 +54,10 @@ export default { this.newNameError = "Name contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `"; } else { let parentPath = this.nodeToRename.substring(0, this.nodeToRename.lastIndexOf('/') + 1); - this.$store.dispatch('moveNode', { - target: parentPath + this.oldName, - direction: parentPath + this.newName + this.$store.dispatch('moveOrCopyNodes', { + targets: [parentPath + this.oldName], + direction: parentPath + this.newName, + keepBytes: false }) .then(() => { this.$bvModal.hide('rename-modal'); diff --git a/vospace-ui-frontend/src/main.js b/vospace-ui-frontend/src/main.js index 7ba1ed9..a25f814 100644 --- a/vospace-ui-frontend/src/main.js +++ b/vospace-ui-frontend/src/main.js @@ -36,13 +36,23 @@ window.renameNode = function(path) { vm.$bvModal.show('rename-modal'); } window.moveNode = function(path) { - store.commit('setNodeToMove', path); - vm.$bvModal.show('move-modal'); + moveOrCopy(path, 'move'); } -window.openNodeInMoveModal = function(event, path) { +window.copyNode = function(path) { + moveOrCopy(path, 'copy'); +} +window.openNodeInMoveOrCopyModal = function(event, path) { event.preventDefault(); event.stopPropagation(); - store.dispatch('openNodeInMoveModal', path); + store.dispatch('openNodeInMoveOrCopyModal', path); +} + +function moveOrCopy(path, moveOrCopy) { + store.commit('setNodesToMoveOrCopy', [path]); + store.commit('setMoveOrCopy', moveOrCopy); + store.commit('setSelectedNotMovableNodes', []); + store.commit('setSelectedNotCopiableNodes', []); + vm.$bvModal.show('move-or-copy-modal'); } export default { diff --git a/vospace-ui-frontend/src/store.js b/vospace-ui-frontend/src/store.js index 21640ef..c2d82b9 100644 --- a/vospace-ui-frontend/src/store.js +++ b/vospace-ui-frontend/src/store.js @@ -29,7 +29,9 @@ export default new Vuex.Store({ nodesLoading: false, asyncButtonEnabled: false, deleteButtonEnabled: false, + moveButtonEnabled: false, archiveButtonEnabled: false, + copyButtonEnabled: false, jobs: [], jobsLoading: true, lastJobsCheckTime: null, @@ -43,9 +45,12 @@ export default new Vuex.Store({ groupWrite: null }, nodeToRename: null, - nodeToMove: null, - nodeToMoveDestination: null, - nodeToMoveDestinationWritable: false, + moveOrCopy: 'move', + nodesToMoveOrCopy: [], + selectedNotMovableNodes: [], + selectedNotCopiableNodes: [], + moveOrCopyDestination: null, + moveOrCopyDestinationWritable: false, archiveType: null, nodesToArchive: [], selectedNotArchivableNodes: [] @@ -72,9 +77,15 @@ export default new Vuex.Store({ setDeleteButtonEnabled(state, value) { state.deleteButtonEnabled = value; }, + setMoveButtonEnabled(state, value) { + state.moveButtonEnabled = value; + }, setArchiveButtonEnabled(state, value) { state.archiveButtonEnabled = value; }, + setCopyButtonEnabled(state, value) { + state.copyButtonEnabled = value; + }, setJobs(state, jobs) { updateArray(state.jobs, jobs); }, @@ -93,6 +104,9 @@ export default new Vuex.Store({ setSelectedUndeletableNodes(state, paths) { updateArray(state.selectedUndeletableNodes, paths); }, + setSelectedNotCopiableNodes(state, paths) { + updateArray(state.selectedNotCopiableNodes, paths); + }, setWritable(state, value) { state.writable = value; }, @@ -104,14 +118,20 @@ export default new Vuex.Store({ setNodeToRename(state, path) { state.nodeToRename = path; }, - setNodeToMove(state, path) { - state.nodeToMove = path; + setMoveOrCopy(state, value) { + state.moveOrCopy = value; + }, + setNodesToMoveOrCopy(state, paths) { + updateArray(state.nodesToMoveOrCopy, paths); + }, + setMoveOrCopyDestination(state, path) { + state.moveOrCopyDestination = path; }, - setNodeToMoveDestination(state, path) { - state.nodeToMoveDestination = path; + setMoveOrCopyDestinationWritable(state, value) { + state.moveOrCopyDestinationWritable = value; }, - setNodeToMoveDestinationWritable(state, value) { - state.nodeToMoveDestinationWritable = value; + setSelectedNotMovableNodes(state, paths) { + updateArray(state.selectedNotMovableNodes, paths); }, setArchiveType(state, type) { state.archiveType = type; @@ -147,7 +167,9 @@ export default new Vuex.Store({ computeButtonsVisibility({ commit }) { commit('setAsyncButtonEnabled', document.querySelectorAll('#nodes input.async:checked').length > 0); commit('setDeleteButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0); + commit('setMoveButtonEnabled', document.querySelectorAll('#nodes input.deletable:checked').length > 0); commit('setArchiveButtonEnabled', document.querySelectorAll('#nodes input:not(.async):checked').length > 0); + commit('setCopyButtonEnabled', document.querySelectorAll('#nodes input:not(.async):checked').length > 0); }, startAsyncRecallJob({ state, commit, dispatch }) { let asyncCheckboxes = document.querySelectorAll('#nodes input.async:checked'); @@ -215,24 +237,27 @@ export default new Vuex.Store({ dispatch('setPath', state.path); }); }, - moveNode({ state, commit, dispatch }, data) { - client.moveNode(data) - .then(job => { - if (job.phase === 'COMPLETED') { + moveOrCopyNodes({ state, commit, dispatch }, data) { + client.moveOrCopyNodes(data) + .then(jobs => { + let uncompletedJobs = jobs.filter(j => j.phase !== 'COMPLETED'); + if (uncompletedJobs.length === 0) { // Reload current node dispatch('setPath', state.path); } else { - main.showInfo('Move operation is taking some time and will be handled in background'); - commit('addJob', job); + main.showInfo('The operation is taking some time and will be handled in background'); + for (let job of uncompletedJobs) { + commit('addJob', job); + } } }); }, - openNodeInMoveModal({ state, commit }, path) { - commit('setNodeToMoveDestination', path); - client.getNodesForMove({ path, nodeToMove: state.nodeToMove }) + openNodeInMoveOrCopyModal({ state, commit }, path) { + commit('setMoveOrCopyDestination', path); + client.getNodesForMoveOrCopy({ path, targets: state.nodesToMoveOrCopy }) .then(res => { - commit('setNodeToMoveDestinationWritable', res.writable); - document.getElementById('move-nodes').outerHTML = res.html; + commit('setMoveOrCopyDestinationWritable', res.writable); + document.getElementById('move-or-copy-nodes').outerHTML = res.html; }); }, createArchive({ state, commit }) { -- GitLab