Merge pull request #6014 from nextcloud/add-copy-action

Allows files/folders to be copied.
This commit is contained in:
Morris Jobke 2017-09-18 14:19:30 +02:00 committed by GitHub
commit f9dc6c456e
8 changed files with 368 additions and 26 deletions

View File

@ -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);
}
});

View File

@ -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.
*
@ -2037,6 +2041,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
*

View File

@ -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>

View File

@ -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');

View File

@ -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;
}

View File

@ -736,6 +736,51 @@
return promise;
},
/**
* 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
*

View File

@ -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>');

View File

@ -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;
}
}
};