summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMorris Jobke <hey@morrisjobke.de>2017-09-18 14:19:30 +0200
committerGitHub <noreply@github.com>2017-09-18 14:19:30 +0200
commitf9dc6c456ecdd4802c4e7ed2e1ceb565480ae82f (patch)
treecd5451067939d2cfa6ae17d78451ecf8343e1f90
parentbdba9871d0da1b62a7ab3c132ceb3b469848f535 (diff)
parent8c576a8d6374cabaa8807455b412603f0e8f1d45 (diff)
downloadnextcloud-server-f9dc6c456ecdd4802c4e7ed2e1ceb565480ae82f.tar.gz
nextcloud-server-f9dc6c456ecdd4802c4e7ed2e1ceb565480ae82f.zip
Merge pull request #6014 from nextcloud/add-copy-action
Allows files/folders to be copied.
-rw-r--r--apps/files/js/fileactions.js15
-rw-r--r--apps/files/js/filelist.js124
-rw-r--r--apps/files/templates/list.php4
-rw-r--r--apps/files/tests/js/filelistSpec.js98
-rw-r--r--core/css/jquery.ocdialog.css3
-rw-r--r--core/js/files/client.js45
-rw-r--r--core/js/jquery.ocdialog.js5
-rw-r--r--core/js/oc-dialogs.js100
8 files changed, 368 insertions, 26 deletions
diff --git a/apps/files/js/fileactions.js b/apps/files/js/fileactions.js
index 3e0cf998254..3da9b06b0d3 100644
--- a/apps/files/js/fileactions.js
+++ b/apps/files/js/fileactions.js
@@ -618,16 +618,21 @@
});
this.registerAction({
- name: 'Move',
- displayName: t('files', 'Move'),
+ name: 'MoveCopy',
+ displayName: t('files', 'Move or copy'),
mime: 'all',
order: -25,
permissions: OC.PERMISSION_UPDATE,
iconClass: 'icon-external',
actionHandler: function (filename, context) {
- OC.dialogs.filepicker(t('files', 'Target folder'), function(targetPath) {
- context.fileList.move(filename, targetPath);
- }, false, "httpd/unix-directory", true);
+ OC.dialogs.filepicker(t('files', 'Target folder'), function(targetPath, type) {
+ if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
+ context.fileList.copy(filename, targetPath);
+ }
+ if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
+ context.fileList.move(filename, targetPath);
+ }
+ }, false, "httpd/unix-directory", true, OC.dialogs.FILEPICKER_TYPE_COPY_MOVE);
}
});
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 95fbc2b7560..48ac0f4e33a 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -337,7 +337,7 @@
this.$el.on('urlChanged', _.bind(this._onUrlChanged, this));
this.$el.find('.select-all').click(_.bind(this._onClickSelectAll, this));
this.$el.find('.download').click(_.bind(this._onClickDownloadSelected, this));
- this.$el.find('.move').click(_.bind(this._onClickMoveSelected, this));
+ this.$el.find('.copy-move').click(_.bind(this._onClickCopyMoveSelected, this));
this.$el.find('.delete-selected').click(_.bind(this._onClickDeleteSelected, this));
this.$el.find('.selectedActions a').tooltip({placement:'top'});
@@ -761,7 +761,7 @@
/**
* Event handler for when clicking on "Move" for the selected files
*/
- _onClickMoveSelected: function(event) {
+ _onClickCopyMoveSelected: function(event) {
var files;
var self = this;
@@ -779,14 +779,17 @@
OCA.Files.FileActions.updateFileActionSpinner(moveFileAction, false);
};
- OCA.Files.FileActions.updateFileActionSpinner(moveFileAction, true);
- OC.dialogs.filepicker(t('files', 'Target folder'), function(targetPath) {
- self.move(files, targetPath, disableLoadingState);
- }, false, "httpd/unix-directory", true);
+ OC.dialogs.filepicker(t('files', 'Target folder'), function(targetPath, type) {
+ if (type === OC.dialogs.FILEPICKER_TYPE_COPY) {
+ self.copy(files, targetPath, disableLoadingState);
+ }
+ if (type === OC.dialogs.FILEPICKER_TYPE_MOVE) {
+ self.move(files, targetPath, disableLoadingState);
+ }
+ }, false, "httpd/unix-directory", true, OC.dialogs.FILEPICKER_TYPE_COPY_MOVE);
return false;
},
-
/**
* Event handler for when clicking on "Delete" for the selected files
*/
@@ -1974,6 +1977,7 @@
}
return index;
},
+
/**
* Moves a file to a given target folder.
*
@@ -2038,6 +2042,112 @@
},
/**
+ * Copies a file to a given target folder.
+ *
+ * @param fileNames array of file names to copy
+ * @param targetPath absolute target path
+ * @param callback to call when copy is finished with success
+ */
+ copy: function(fileNames, targetPath, callback) {
+ var self = this;
+ var filesToNotify = [];
+ var count = 0;
+
+ var dir = this.getCurrentDirectory();
+ if (dir.charAt(dir.length - 1) !== '/') {
+ dir += '/';
+ }
+ var target = OC.basename(targetPath);
+ if (!_.isArray(fileNames)) {
+ fileNames = [fileNames];
+ }
+ _.each(fileNames, function(fileName) {
+ var $tr = self.findFileEl(fileName);
+ self.showFileBusyState($tr, true);
+ if (targetPath.charAt(targetPath.length - 1) !== '/') {
+ // make sure we move the files into the target dir,
+ // not overwrite it
+ targetPath = targetPath + '/';
+ }
+ self.filesClient.copy(dir + fileName, targetPath + fileName)
+ .done(function () {
+ filesToNotify.push(fileName);
+
+ // if still viewing the same directory
+ if (OC.joinPaths(self.getCurrentDirectory(), '/') === dir) {
+ // recalculate folder size
+ var oldFile = self.findFileEl(target);
+ var newFile = self.findFileEl(fileName);
+ var oldSize = oldFile.data('size');
+ var newSize = oldSize + newFile.data('size');
+ oldFile.data('size', newSize);
+ oldFile.find('td.filesize').text(OC.Util.humanFileSize(newSize));
+ }
+ })
+ .fail(function(status) {
+ if (status === 412) {
+ // TODO: some day here we should invoke the conflict dialog
+ OC.Notification.show(t('files', 'Could not copy "{file}", target exists',
+ {file: fileName}), {type: 'error'}
+ );
+ } else {
+ OC.Notification.show(t('files', 'Could not copy "{file}"',
+ {file: fileName}), {type: 'error'}
+ );
+ }
+ })
+ .always(function() {
+ self.showFileBusyState($tr, false);
+ count++;
+
+ /**
+ * We only show the notifications once the last file has been copied
+ */
+ if (count === fileNames.length) {
+ // Remove leading and ending /
+ if (targetPath.slice(0, 1) === '/') {
+ targetPath = targetPath.slice(1, targetPath.length);
+ }
+ if (targetPath.slice(-1) === '/') {
+ targetPath = targetPath.slice(0, -1);
+ }
+
+ if (filesToNotify.length > 0) {
+ // Since there's no visual indication that the files were copied, let's send some notifications !
+ if (filesToNotify.length === 1) {
+ OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
+ {
+ origin: filesToNotify[0],
+ destination: targetPath
+ }
+ ), {timeout: 10});
+ } else if (filesToNotify.length > 0 && filesToNotify.length < 3) {
+ OC.Notification.show(t('files', 'Copied {origin} inside {destination}',
+ {
+ origin: filesToNotify.join(', '),
+ destination: targetPath
+ }
+ ), {timeout: 10});
+ } else {
+ OC.Notification.show(t('files', 'Copied {origin} and {nbfiles} other files inside {destination}',
+ {
+ origin: filesToNotify[0],
+ nbfiles: filesToNotify.length - 1,
+ destination: targetPath
+ }
+ ), {timeout: 10});
+ }
+ }
+ }
+ });
+ });
+
+ if (callback) {
+ callback();
+ }
+ },
+
+ /**
* Updates the given row with the given file info
*
* @param {Object} $tr row element
diff --git a/apps/files/templates/list.php b/apps/files/templates/list.php
index 67c330c38c7..f3b6759644c 100644
--- a/apps/files/templates/list.php
+++ b/apps/files/templates/list.php
@@ -47,9 +47,9 @@
</label>
<a class="name sort columntitle" data-sort="name"><span><?php p($l->t( 'Name' )); ?></span><span class="sort-indicator"></span></a>
<span id="selectedActionsList" class="selectedActions">
- <a href="" class="move">
+ <a href="" class="copy-move">
<span class="icon icon-external"></span>
- <span><?php p($l->t('Move'))?></span>
+ <span><?php p($l->t('Move or copy'))?></span>
</a>
<a href="" class="download">
<span class="icon icon-download"></span>
diff --git a/apps/files/tests/js/filelistSpec.js b/apps/files/tests/js/filelistSpec.js
index a12c0ff49b6..5061d70c4c7 100644
--- a/apps/files/tests/js/filelistSpec.js
+++ b/apps/files/tests/js/filelistSpec.js
@@ -853,6 +853,104 @@ describe('OCA.Files.FileList tests', function() {
.toEqual(OC.imagePath('core', 'filetypes/text.svg'));
});
});
+
+ describe('Copying files', function() {
+ var deferredCopy;
+ var copyStub;
+
+ beforeEach(function() {
+ deferredCopy = $.Deferred();
+ copyStub = sinon.stub(filesClient, 'copy').returns(deferredCopy.promise());
+
+ fileList.setFiles(testFiles);
+ });
+ afterEach(function() {
+ copyStub.restore();
+ });
+
+ it('Copies single file to target folder', function() {
+ fileList.copy('One.txt', '/somedir');
+
+ expect(copyStub.calledOnce).toEqual(true);
+ expect(copyStub.getCall(0).args[0]).toEqual('/subdir/One.txt');
+ expect(copyStub.getCall(0).args[1]).toEqual('/somedir/One.txt');
+
+ deferredCopy.resolve(201);
+
+ // File is still here
+ expect(fileList.findFileEl('One.txt').length).toEqual(1);
+
+ // folder size has increased
+ expect(fileList.findFileEl('somedir').data('size')).toEqual(262);
+ expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B');
+
+ // Copying sents a notification to tell that we've successfully copied file
+ expect(notificationStub.notCalled).toEqual(false);
+ });
+ it('Copies list of files to target folder', function() {
+ var deferredCopy1 = $.Deferred();
+ var deferredCopy2 = $.Deferred();
+ copyStub.onCall(0).returns(deferredCopy1.promise());
+ copyStub.onCall(1).returns(deferredCopy2.promise());
+
+ fileList.copy(['One.txt', 'Two.jpg'], '/somedir');
+
+ expect(copyStub.calledTwice).toEqual(true);
+ expect(copyStub.getCall(0).args[0]).toEqual('/subdir/One.txt');
+ expect(copyStub.getCall(0).args[1]).toEqual('/somedir/One.txt');
+ expect(copyStub.getCall(1).args[0]).toEqual('/subdir/Two.jpg');
+ expect(copyStub.getCall(1).args[1]).toEqual('/somedir/Two.jpg');
+
+ deferredCopy1.resolve(201);
+
+ expect(fileList.findFileEl('One.txt').length).toEqual(1);
+
+ // folder size has increased during copy
+ expect(fileList.findFileEl('somedir').data('size')).toEqual(262);
+ expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('262 B');
+
+ deferredCopy2.resolve(201);
+
+ expect(fileList.findFileEl('Two.jpg').length).toEqual(1);
+
+ // folder size has increased
+ expect(fileList.findFileEl('somedir').data('size')).toEqual(12311);
+ expect(fileList.findFileEl('somedir').find('.filesize').text()).toEqual('12 KB');
+
+ expect(notificationStub.notCalled).toEqual(false);
+ });
+ it('Shows notification if a file could not be copied', function() {
+ fileList.copy('One.txt', '/somedir');
+
+ expect(copyStub.calledOnce).toEqual(true);
+
+ deferredCopy.reject(409);
+
+ expect(fileList.findFileEl('One.txt').length).toEqual(1);
+
+ expect(notificationStub.calledOnce).toEqual(true);
+ expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"');
+ });
+ it('Restores thumbnail if a file could not be copied', function() {
+ fileList.copy('One.txt', '/somedir');
+
+ expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail')))
+ .toEqual(OC.imagePath('core', 'loading.gif'));
+
+ expect(copyStub.calledOnce).toEqual(true);
+
+ deferredCopy.reject(409);
+
+ expect(fileList.findFileEl('One.txt').length).toEqual(1);
+
+ expect(notificationStub.calledOnce).toEqual(true);
+ expect(notificationStub.getCall(0).args[0]).toEqual('Could not copy "One.txt"');
+
+ expect(OC.TestUtil.getImageUrl(fileList.findFileEl('One.txt').find('.thumbnail')))
+ .toEqual(OC.imagePath('core', 'filetypes/text.svg'));
+ });
+ });
+
describe('Update file', function() {
it('does not change summary', function() {
var $summary = $('#filestable .summary');
diff --git a/core/css/jquery.ocdialog.css b/core/css/jquery.ocdialog.css
index 487bc1c4f69..2100a3db7a6 100644
--- a/core/css/jquery.ocdialog.css
+++ b/core/css/jquery.ocdialog.css
@@ -38,6 +38,9 @@
.oc-dialog-buttonrow.twobuttons button:nth-child(1) {
float: left;
}
+.oc-dialog-buttonrow.twobuttons.aside button:nth-child(1) {
+ float: none;
+}
.oc-dialog-buttonrow.twobuttons button:nth-child(2) {
float: right;
}
diff --git a/core/js/files/client.js b/core/js/files/client.js
index 176cabf04b1..dc9f6ade641 100644
--- a/core/js/files/client.js
+++ b/core/js/files/client.js
@@ -737,6 +737,51 @@
},
/**
+ * Copies path to another path
+ *
+ * @param {String} path path to copy
+ * @param {String} destinationPath destination path
+ * @param {boolean} [allowOverwrite=false] true to allow overwriting,
+ * false otherwise
+ *
+ * @return {Promise} promise
+ */
+ copy: function (path, destinationPath, allowOverwrite) {
+ if (!path) {
+ throw 'Missing argument "path"';
+ }
+ if (!destinationPath) {
+ throw 'Missing argument "destinationPath"';
+ }
+
+ var self = this;
+ var deferred = $.Deferred();
+ var promise = deferred.promise();
+ var headers = {
+ 'Destination' : this._buildUrl(destinationPath)
+ };
+
+ if (!allowOverwrite) {
+ headers.Overwrite = 'F';
+ }
+
+ this._client.request(
+ 'COPY',
+ this._buildUrl(path),
+ headers
+ ).then(
+ function(response) {
+ if (self._isSuccessStatus(response.status)) {
+ deferred.resolve(response.status);
+ } else {
+ deferred.reject(response.status);
+ }
+ }
+ );
+ return promise;
+ },
+
+ /**
* Add a file info parser function
*
* @param {OC.Files.Client~parseFileInfo>}
diff --git a/core/js/jquery.ocdialog.js b/core/js/jquery.ocdialog.js
index b54cce2c0ca..555b35e59ff 100644
--- a/core/js/jquery.ocdialog.js
+++ b/core/js/jquery.ocdialog.js
@@ -130,6 +130,11 @@
});
this._setSizes();
break;
+ case 'style':
+ if (value.buttons !== undefined) {
+ this.$buttonrow.addClass(value.buttons);
+ }
+ break;
case 'closeButton':
if(value) {
var $closeButton = $('<a class="oc-dialog-close"></a>');
diff --git a/core/js/oc-dialogs.js b/core/js/oc-dialogs.js
index 5fc224e38bf..1bc1399466d 100644
--- a/core/js/oc-dialogs.js
+++ b/core/js/oc-dialogs.js
@@ -29,6 +29,12 @@ var OCdialogs = {
// dialog button types
YES_NO_BUTTONS: 70,
OK_BUTTONS: 71,
+
+ FILEPICKER_TYPE_CHOOSE: 1,
+ FILEPICKER_TYPE_MOVE: 2,
+ FILEPICKER_TYPE_COPY: 3,
+ FILEPICKER_TYPE_COPY_MOVE: 4,
+
// used to name each dialog
dialogsCounter: 0,
/**
@@ -174,13 +180,19 @@ var OCdialogs = {
* @param multiselect whether it should be possible to select multiple files
* @param mimetypeFilter mimetype to filter by - directories will always be included
* @param modal make the dialog modal
+ * @param type Type of file picker : Choose, copy, move, copy and move
*/
- filepicker:function(title, callback, multiselect, mimetypeFilter, modal) {
+ filepicker:function(title, callback, multiselect, mimetypeFilter, modal, type) {
var self = this;
// avoid opening the picker twice
if (this.filepicker.loading) {
return;
}
+
+ if (type === undefined) {
+ type = this.FILEPICKER_TYPE_CHOOSE;
+ }
+
this.filepicker.loading = true;
this.filepicker.filesClient = (OCA.Sharing && OCA.Sharing.PublicApp && OCA.Sharing.PublicApp.fileList)? OCA.Sharing.PublicApp.fileList.filesClient: OC.Files.getClient();
$.when(this._getFilePickerTemplate()).then(function($tmpl) {
@@ -210,15 +222,17 @@ var OCdialogs = {
self.$filePicker.ready(function() {
self.$filelist = self.$filePicker.find('.filelist tbody');
self.$dirTree = self.$filePicker.find('.dirtree');
- self.$dirTree.on('click', 'div:not(:last-child)', self, self._handleTreeListSelect.bind(self));
+ self.$dirTree.on('click', 'div:not(:last-child)', self, function (event) {
+ self._handleTreeListSelect(event, type);
+ });
self.$filelist.on('click', 'tr', function(event) {
- self._handlePickerClick(event, $(this));
+ self._handlePickerClick(event, $(this), type);
});
self._fillFilePicker('');
});
// build buttons
- var functionToCall = function() {
+ var functionToCall = function(returnType) {
if (callback !== undefined) {
var datapath;
if (multiselect === true) {
@@ -233,15 +247,46 @@ var OCdialogs = {
datapath += '/' + selectedName;
}
}
- callback(datapath);
+ callback(datapath, returnType);
self.$filePicker.ocdialog('close');
}
};
- var buttonlist = [{
- text: t('core', 'Choose'),
- click: functionToCall,
- defaultButton: true
- }];
+
+ var chooseCallback = function () {
+ functionToCall(OCdialogs.FILEPICKER_TYPE_CHOOSE);
+ };
+
+ var copyCallback = function () {
+ functionToCall(OCdialogs.FILEPICKER_TYPE_COPY);
+ };
+
+ var moveCallback = function () {
+ functionToCall(OCdialogs.FILEPICKER_TYPE_MOVE);
+ };
+
+ var buttonlist = [];
+ if (type === OCdialogs.FILEPICKER_TYPE_CHOOSE) {
+ buttonlist.push({
+ text: t('core', 'Choose'),
+ click: chooseCallback,
+ defaultButton: true
+ });
+ } else {
+ if (type === OCdialogs.FILEPICKER_TYPE_COPY || type === OCdialogs.FILEPICKER_TYPE_COPY_MOVE) {
+ buttonlist.push({
+ text: t('core', 'Copy'),
+ click: copyCallback,
+ defaultButton: false
+ });
+ }
+ if (type === OCdialogs.FILEPICKER_TYPE_MOVE || type === OCdialogs.FILEPICKER_TYPE_COPY_MOVE) {
+ buttonlist.push({
+ text: t('core', 'Move'),
+ click: moveCallback,
+ defaultButton: true
+ });
+ }
+ }
self.$filePicker.ocdialog({
closeOnEscape: true,
@@ -250,6 +295,9 @@ var OCdialogs = {
height: 500,
modal: modal,
buttons: buttonlist,
+ style: {
+ buttons: 'aside',
+ },
close: function() {
try {
$(this).ocdialog('destroy').remove();
@@ -879,12 +927,13 @@ var OCdialogs = {
/**
* handle selection made in the tree list
*/
- _handleTreeListSelect:function(event) {
+ _handleTreeListSelect:function(event, type) {
var self = event.data;
var dir = $(event.target).parent().data('dir');
self._fillFilePicker(dir);
var getOcDialog = (event.target).closest('.oc-dialog');
var buttonEnableDisable = $('.primary', getOcDialog);
+ this._changeButtonsText(type, dir.split(/[/]+/).pop());
if (this.$filePicker.data('mimetype') === "httpd/unix-directory") {
buttonEnableDisable.prop("disabled", false);
} else {
@@ -894,7 +943,7 @@ var OCdialogs = {
/**
* handle clicks made in the filepicker
*/
- _handlePickerClick:function(event, $element) {
+ _handlePickerClick:function(event, $element, type) {
var getOcDialog = this.$filePicker.closest('.oc-dialog');
var buttonEnableDisable = getOcDialog.find('.primary');
if ($element.data('type') === 'file') {
@@ -905,11 +954,38 @@ var OCdialogs = {
buttonEnableDisable.prop("disabled", false);
} else if ( $element.data('type') === 'dir' ) {
this._fillFilePicker(this.$filePicker.data('path') + '/' + $element.data('entryname'));
+ this._changeButtonsText(type, $element.data('entryname'));
if (this.$filePicker.data('mimetype') === "httpd/unix-directory") {
buttonEnableDisable.prop("disabled", false);
} else {
buttonEnableDisable.prop("disabled", true);
}
}
+ },
+
+ /**
+ * Handle
+ * @param type of action
+ * @param dir on which to change buttons text
+ * @private
+ */
+ _changeButtonsText: function(type, dir) {
+ var copyText = dir === '' ? t('core', 'Copy') : t('core', 'Copy to {folder}', {folder: dir});
+ var moveText = dir === '' ? t('core', 'Move') : t('core', 'Move to {folder}', {folder: dir});
+ var buttons = $('.oc-dialog-buttonrow button');
+ switch (type) {
+ case this.FILEPICKER_TYPE_CHOOSE:
+ break;
+ case this.FILEPICKER_TYPE_COPY:
+ buttons.text(copyText);
+ break;
+ case this.FILEPICKER_TYPE_MOVE:
+ buttons.text(moveText);
+ break;
+ case this.FILEPICKER_TYPE_COPY_MOVE:
+ buttons.eq(0).text(copyText);
+ buttons.eq(1).text(moveText);
+ break;
+ }
}
};