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 5133781348e9d779e2bed0582d642b4bdf168a75..f4cb5f1d7779ef47bc38315adf5c5d5dca8f8573 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
@@ -210,6 +210,7 @@ public class NodesController extends BaseController {
     @PostMapping(value = "/moveOrCopy")
     public ResponseEntity<List<Job>> moveOrCopyNodes(@RequestBody MoveOrCopyRequest request) throws Exception {
 
+        // Creates a transfer request for each copy or move operation
         CompletableFuture<JobSummary>[] futureJobs = request.getTargets().stream().map(t -> {
             String target = urlEncodePath(t);
             String direction = urlEncodePath(request.getDirection());
@@ -220,12 +221,21 @@ public class NodesController extends BaseController {
             return CompletableFuture.supplyAsync(() -> client.startTransferJob(transfer), Runnable::run);
         }).collect(Collectors.toList()).toArray(CompletableFuture[]::new);
 
+        // starts all HTTP requests in parallel
         CompletableFuture.allOf(futureJobs).join();
 
+        // parses all the responses and populates a list of jobs to check the
+        // completion status using polling mechanism
         List<JobSummary> jobs = Stream.of(futureJobs).map(j -> j.join()).collect(Collectors.toList());
 
+        // Job statuses are checked for some time and if completion takes too much
+        // time sends the execution phase to the UI and let it handles the polling.
+        // Since CompletableFutures can't really be aborted (see cancel method
+        // documentation) an AtomicReference containing a boolean flag it is
+        // passed to handle polling abortion.
         AtomicReference<Boolean> cancelled = new AtomicReference<>(false);
 
+        // CompletableFuture that triggers the timeout
         CompletableFuture timeout = CompletableFuture.runAsync(() -> {
             try {
                 Thread.sleep(pollingTimeout * 1000);
@@ -235,6 +245,7 @@ public class NodesController extends BaseController {
         });
 
         try {
+            // performs polling of job statuses or timeout
             CompletableFuture.anyOf(jobsPolling(jobs, cancelled), timeout).join();
         } catch (CompletionException ex) {
             if (ex.getCause() != null && ex.getCause() instanceof VOSpaceException) {
@@ -243,8 +254,6 @@ public class NodesController extends BaseController {
             throw ex;
         }
 
-        // Try to perform polling until completion. If it takes too much time
-        // sends the execution phase to the UI and let it handles the polling.
         Job.JobType type = request.isKeepBytes() ? Job.JobType.COPY : Job.JobType.MOVE;
 
         return ResponseEntity.ok(jobs.stream().map(j -> new Job(j, type))
diff --git a/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue
index 2534b926cd5eca7f597a08edc51c901cd64f8e3b..5bf47a934ef7f984d5e5fa774422322579323f82 100644
--- a/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue
+++ b/vospace-ui-frontend/src/components/modal/MoveOrCopyModal.vue
@@ -5,6 +5,12 @@
 -->
 <template>
 <b-modal id="move-or-copy-modal" :title="title" :okTitle="okTitle" @show="afterShow" @ok.prevent="moveOrCopyNodes" :ok-disabled="!writable" size="lg">
+  <b-form inline v-if="this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy'" class="mb-3">
+    <label class="w-25" for="new-name-input">New name</label>
+    <b-form-input v-model.trim="newName" id="new-name-input" class="w-75" aria-describedby="new-name-input-feedback" :state="newNameState" v-on:input="resetNewNameError" @keydown.native.enter="moveOrCopyNodes">
+    </b-form-input>
+    <b-form-invalid-feedback id="new-folder-name-input-feedback" class="text-right">{{newNameError}}</b-form-invalid-feedback>
+  </b-form>
   <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>
@@ -66,13 +72,31 @@ export default {
         }
       }
       return items;
+    },
+    newNameState() {
+      if (this.newNameError) {
+        return false;
+      }
+      return null;
+    }
+  },
+  data() {
+    return {
+      newName: null,
+      newNameError: null
     }
   },
   methods: {
     afterShow() {
       // starts from parent path
       let firstSelectedNode = this.nodesToMoveOrCopy[0];
-      this.$store.dispatch('openNodeInMoveOrCopyModal', firstSelectedNode.substring(0, firstSelectedNode.lastIndexOf('/')));
+      let lastSlashPos = firstSelectedNode.lastIndexOf('/');
+      this.$store.dispatch('openNodeInMoveOrCopyModal', firstSelectedNode.substring(0, lastSlashPos));
+      if (this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy') {
+        this.newName = firstSelectedNode.substring(lastSlashPos + 1);
+      } else {
+        this.newName = null;
+      }
     },
     breadcrumbClick(i) {
       let pathSplit = this.destinationPath.split('/');
@@ -84,14 +108,31 @@ export default {
       window.openNodeInMoveOrCopyModal(event, path);
     },
     moveOrCopyNodes() {
+      let direction = this.destinationPath;
+
+      if (this.nodesToMoveOrCopy.length === 1 && this.moveOrCopy === 'copy') {
+        if (!this.newName) {
+          this.newNameError = "Name is required";
+          return;
+        } else if (/[<>?":\\/`|'*]/.test(this.newName)) {
+          this.newNameError = "Name contains an illegal character. Following characters are not allowed: < > ? \" : \\ / | ' * `";
+          return;
+        }
+
+        direction += '/' + this.newName;
+      }
+
       this.$store.dispatch('moveOrCopyNodes', {
           targets: this.nodesToMoveOrCopy,
-          direction: this.destinationPath,
+          direction,
           keepBytes: this.moveOrCopy === 'copy'
         })
         .then(() => {
           this.$bvModal.hide('move-or-copy-modal');
         })
+    },
+    resetNewNameError() {
+      this.newNameError = null;
     }
   }
 }