Merge pull request #6014 from nextcloud/add-copy-action
Allows files/folders to be copied.
This commit is contained in:
commit
f9dc6c456e
|
@ -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) {
|
||||
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);
|
||||
}
|
||||
}, false, "httpd/unix-directory", true, OC.dialogs.FILEPICKER_TYPE_COPY_MOVE);
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -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) {
|
||||
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);
|
||||
}
|
||||
}, 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
|
||||
*
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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');
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
|
||||
*
|
||||
|
|
|
@ -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>');
|
||||
|
|
|
@ -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 = [{
|
||||
|
||||
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: functionToCall,
|
||||
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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
|
Loading…
Reference in New Issue