Allows files/folders to be copied.tags/v13.0.0beta1
@@ -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); | |||
} | |||
}); | |||
@@ -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 | |||
* |
@@ -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 = [{ | |||
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; | |||
} | |||
} | |||
}; |