summaryrefslogtreecommitdiffstats
path: root/apps
diff options
context:
space:
mode:
authorVincent Petry <pvince81@owncloud.com>2015-09-01 19:29:55 +0200
committerVincent Petry <pvince81@owncloud.com>2015-09-03 16:47:24 +0200
commit310d79728447ecf69f18d0b61a527397bd961888 (patch)
tree805b2a0a40ed5ce7acb58afb90ad7c18e760e037 /apps
parente9e42fff61a922f11a3b1014d810562537950b6a (diff)
downloadnextcloud-server-310d79728447ecf69f18d0b61a527397bd961888.tar.gz
nextcloud-server-310d79728447ecf69f18d0b61a527397bd961888.zip
Add versions tab to files sidebar
- move versions to a tab in the files sidebar - added mechanism to auto-update the row in the FileList whenever values are set to the FileInfoModel given to the sidebar - updated tags/favorite action to make use of that new mechanism
Diffstat (limited to 'apps')
-rw-r--r--apps/files/js/detailsview.js2
-rw-r--r--apps/files/js/detailtabview.js7
-rw-r--r--apps/files/js/filelist.js40
-rw-r--r--apps/files/js/tagsplugin.js16
-rw-r--r--apps/files/tests/js/tagspluginspec.js18
-rw-r--r--apps/files_versions/ajax/getVersions.php2
-rw-r--r--apps/files_versions/ajax/preview.php5
-rw-r--r--apps/files_versions/css/versions.css27
-rw-r--r--apps/files_versions/js/filesplugin.js34
-rw-r--r--apps/files_versions/js/versioncollection.js91
-rw-r--r--apps/files_versions/js/versionmodel.js77
-rw-r--r--apps/files_versions/js/versions.js193
-rw-r--r--apps/files_versions/js/versionstabview.js198
-rw-r--r--apps/files_versions/lib/hooks.php12
-rw-r--r--apps/files_versions/tests/js/versioncollectionSpec.js161
-rw-r--r--apps/files_versions/tests/js/versionmodelSpec.js96
-rw-r--r--apps/files_versions/tests/js/versionstabviewSpec.js208
17 files changed, 966 insertions, 221 deletions
diff --git a/apps/files/js/detailsview.js b/apps/files/js/detailsview.js
index 83d7fd4a178..3a775c29ec6 100644
--- a/apps/files/js/detailsview.js
+++ b/apps/files/js/detailsview.js
@@ -35,7 +35,7 @@
var DetailsView = OC.Backbone.View.extend({
id: 'app-sidebar',
tabName: 'div',
- className: 'detailsView',
+ className: 'detailsView scroll-container',
_template: null,
diff --git a/apps/files/js/detailtabview.js b/apps/files/js/detailtabview.js
index b0e170bc4e7..449047cf252 100644
--- a/apps/files/js/detailtabview.js
+++ b/apps/files/js/detailtabview.js
@@ -84,6 +84,13 @@
*/
getFileInfo: function() {
return this.model;
+ },
+
+ /**
+ * Load the next page of results
+ */
+ nextPage: function() {
+ // load the next page, if applicable
}
});
DetailTabView._TAB_COUNT = 0;
diff --git a/apps/files/js/filelist.js b/apps/files/js/filelist.js
index 9593ee79e66..3f0ee932d1e 100644
--- a/apps/files/js/filelist.js
+++ b/apps/files/js/filelist.js
@@ -291,6 +291,7 @@
* @return {OCA.Files.FileInfoModel} file info model
*/
getModelForFile: function(fileName) {
+ var self = this;
var $tr;
// jQuery object ?
if (fileName.is) {
@@ -318,6 +319,21 @@
if (!model.has('path')) {
model.set('path', this.getCurrentDirectory(), {silent: true});
}
+
+ model.on('change', function(model) {
+ // re-render row
+ var highlightState = $tr.hasClass('highlighted');
+ $tr = self.updateRow(
+ $tr,
+ _.extend({isPreviewAvailable: true}, model.toJSON()),
+ {updateSummary: true, silent: false, animate: true}
+ );
+ $tr.toggleClass('highlighted', highlightState);
+ });
+ model.on('busy', function(model, state) {
+ self.showFileBusyState($tr, state);
+ });
+
return model;
},
@@ -341,6 +357,9 @@
if (!fileName) {
OC.Apps.hideAppSidebar(this._detailsView.$el);
this._detailsView.setFileInfo(null);
+ if (this._currentFileModel) {
+ this._currentFileModel.off();
+ }
this._currentFileModel = null;
return;
}
@@ -1223,6 +1242,10 @@
reload: function() {
this._selectedFiles = {};
this._selectionSummary.clear();
+ if (this._currentFileModel) {
+ this._currentFileModel.off();
+ }
+ this._currentFileModel = null;
this.$el.find('.select-all').prop('checked', false);
this.showMask();
if (this._reloadCall) {
@@ -1555,6 +1578,23 @@
},
/**
+ * Updates the given row with the given file info
+ *
+ * @param {Object} $tr row element
+ * @param {OCA.Files.FileInfo} fileInfo file info
+ * @param {Object} options options
+ *
+ * @return {Object} new row element
+ */
+ updateRow: function($tr, fileInfo, options) {
+ this.files.splice($tr.index(), 1);
+ $tr.remove();
+ $tr = this.add(fileInfo, _.extend({updateSummary: false, silent: true}, options));
+ this.$fileList.trigger($.Event('fileActionsReady', {fileList: this, $files: $tr}));
+ return $tr;
+ },
+
+ /**
* Triggers file rename input field for the given file name.
* If the user enters a new name, the file will be renamed.
*
diff --git a/apps/files/js/tagsplugin.js b/apps/files/js/tagsplugin.js
index 609e38ca9a9..9f45da9a6e2 100644
--- a/apps/files/js/tagsplugin.js
+++ b/apps/files/js/tagsplugin.js
@@ -92,6 +92,7 @@
actionHandler: function(fileName, context) {
var $actionEl = context.$file.find('.action-favorite');
var $file = context.$file;
+ var fileInfo = context.fileList.files[$file.index()];
var dir = context.dir || context.fileList.getCurrentDirectory();
var tags = $file.attr('data-tags');
if (_.isUndefined(tags)) {
@@ -106,9 +107,11 @@
} else {
tags.push(OC.TAG_FAVORITE);
}
+
+ // pre-toggle the star
toggleStar($actionEl, !isFavorite);
- context.fileInfoModel.set('tags', tags);
+ context.fileInfoModel.trigger('busy', context.fileInfoModel, true);
self.applyFileTags(
dir + '/' + fileName,
@@ -116,17 +119,16 @@
$actionEl,
isFavorite
).then(function(result) {
+ context.fileInfoModel.trigger('busy', context.fileInfoModel, false);
// response from server should contain updated tags
var newTags = result.tags;
if (_.isUndefined(newTags)) {
newTags = tags;
}
- var fileInfo = context.fileList.files[$file.index()];
- // read latest state from result
- toggleStar($actionEl, (newTags.indexOf(OC.TAG_FAVORITE) >= 0));
- $file.attr('data-tags', newTags.join('|'));
- $file.attr('data-favorite', !isFavorite);
- fileInfo.tags = newTags;
+ context.fileInfoModel.set({
+ 'tags': newTags,
+ 'favorite': !isFavorite
+ });
});
}
});
diff --git a/apps/files/tests/js/tagspluginspec.js b/apps/files/tests/js/tagspluginspec.js
index 950fb754253..533aa63362c 100644
--- a/apps/files/tests/js/tagspluginspec.js
+++ b/apps/files/tests/js/tagspluginspec.js
@@ -79,12 +79,12 @@ describe('OCA.Files.TagsPlugin tests', function() {
it('sends request to server and updates icon', function() {
var request;
fileList.setFiles(testFiles);
- $tr = fileList.$el.find('tbody tr:first');
- $action = $tr.find('.action-favorite');
+ var $tr = fileList.findFileEl('One.txt');
+ var $action = $tr.find('.action-favorite');
$action.click();
expect(fakeServer.requests.length).toEqual(1);
- var request = fakeServer.requests[0];
+ request = fakeServer.requests[0];
expect(JSON.parse(request.requestBody)).toEqual({
tags: ['tag1', 'tag2', OC.TAG_FAVORITE]
});
@@ -92,12 +92,18 @@ describe('OCA.Files.TagsPlugin tests', function() {
tags: ['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]
}));
+ // re-read the element as it was re-inserted
+ $tr = fileList.findFileEl('One.txt');
+ $action = $tr.find('.action-favorite');
+
expect($tr.attr('data-favorite')).toEqual('true');
expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]);
expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3', OC.TAG_FAVORITE]);
expect($action.find('img').attr('src')).toEqual(OC.imagePath('core', 'actions/starred'));
$action.click();
+
+ expect(fakeServer.requests.length).toEqual(2);
request = fakeServer.requests[1];
expect(JSON.parse(request.requestBody)).toEqual({
tags: ['tag1', 'tag2', 'tag3']
@@ -106,7 +112,11 @@ describe('OCA.Files.TagsPlugin tests', function() {
tags: ['tag1', 'tag2', 'tag3']
}));
- expect($tr.attr('data-favorite')).toEqual('false');
+ // re-read the element as it was re-inserted
+ $tr = fileList.findFileEl('One.txt');
+ $action = $tr.find('.action-favorite');
+
+ expect($tr.attr('data-favorite')).toBeFalsy();
expect($tr.attr('data-tags').split('|')).toEqual(['tag1', 'tag2', 'tag3']);
expect(fileList.files[0].tags).toEqual(['tag1', 'tag2', 'tag3']);
expect($action.find('img').attr('src')).toEqual(OC.imagePath('core', 'actions/star'));
diff --git a/apps/files_versions/ajax/getVersions.php b/apps/files_versions/ajax/getVersions.php
index 20d60240179..59bd30f434f 100644
--- a/apps/files_versions/ajax/getVersions.php
+++ b/apps/files_versions/ajax/getVersions.php
@@ -44,6 +44,6 @@ if( $versions ) {
} else {
- \OCP\JSON::success(array('data' => array('versions' => false, 'endReached' => true)));
+ \OCP\JSON::success(array('data' => array('versions' => [], 'endReached' => true)));
}
diff --git a/apps/files_versions/ajax/preview.php b/apps/files_versions/ajax/preview.php
index 8a9a5fba14c..2f33f0278ef 100644
--- a/apps/files_versions/ajax/preview.php
+++ b/apps/files_versions/ajax/preview.php
@@ -53,7 +53,10 @@ try {
$preview->setScalingUp($scalingUp);
$preview->showPreview();
-}catch(\Exception $e) {
+} catch (\OCP\Files\NotFoundException $e) {
+ \OC_Response::setStatus(404);
+ \OCP\Util::writeLog('core', $e->getmessage(), \OCP\Util::DEBUG);
+} catch (\Exception $e) {
\OC_Response::setStatus(500);
\OCP\Util::writeLog('core', $e->getmessage(), \OCP\Util::DEBUG);
}
diff --git a/apps/files_versions/css/versions.css b/apps/files_versions/css/versions.css
index e3ccfc3c864..ec0f0cc9896 100644
--- a/apps/files_versions/css/versions.css
+++ b/apps/files_versions/css/versions.css
@@ -1,19 +1,18 @@
-#dropdown.drop-versions {
- width: 360px;
+.versionsTabView .clear-float {
+ clear: both;
}
-
-#found_versions li {
+.versionsTabView li {
width: 100%;
cursor: default;
height: 56px;
float: left;
border-bottom: 1px solid rgba(100,100,100,.1);
}
-#found_versions li:last-child {
+.versionsTabView li:last-child {
border-bottom: none;
}
-#found_versions li > * {
+.versionsTabView li > * {
padding: 7px;
float: left;
vertical-align: top;
@@ -22,34 +21,34 @@
opacity: .5;
}
-#found_versions li > a,
-#found_versions li > span {
+.versionsTabView li > a,
+.versionsTabView li > span {
padding: 17px 7px;
}
-#found_versions li > *:hover,
-#found_versions li > *:focus {
+.versionsTabView li > *:hover,
+.versionsTabView li > *:focus {
-ms-filter: "progid:DXImageTransform.Microsoft.Alpha(Opacity=100)";
filter: alpha(opacity=100);
opacity: 1;
}
-#found_versions img {
+.versionsTabView img {
cursor: pointer;
padding-right: 4px;
}
-#found_versions img.preview {
+.versionsTabView img.preview {
cursor: default;
opacity: 1;
}
-#found_versions .versionDate {
+.versionsTabView .versionDate {
min-width: 100px;
vertical-align: text-bottom;
}
-#found_versions .revertVersion {
+.versionsTabView .revertVersion {
cursor: pointer;
float: right;
max-width: 130px;
diff --git a/apps/files_versions/js/filesplugin.js b/apps/files_versions/js/filesplugin.js
new file mode 100644
index 00000000000..42075ce6462
--- /dev/null
+++ b/apps/files_versions/js/filesplugin.js
@@ -0,0 +1,34 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ OCA.Versions = OCA.Versions || {};
+
+ /**
+ * @namespace
+ */
+ OCA.Versions.Util = {
+ /**
+ * Initialize the versions plugin.
+ *
+ * @param {OCA.Files.FileList} fileList file list to be extended
+ */
+ attach: function(fileList) {
+ if (fileList.id === 'trashbin' || fileList.id === 'files.public') {
+ return;
+ }
+
+ fileList.registerTabView(new OCA.Versions.VersionsTabView('versionsTabView'));
+ }
+ };
+})();
+
+OC.Plugins.register('OCA.Files.FileList', OCA.Versions.Util);
+
diff --git a/apps/files_versions/js/versioncollection.js b/apps/files_versions/js/versioncollection.js
new file mode 100644
index 00000000000..3f8214cde8c
--- /dev/null
+++ b/apps/files_versions/js/versioncollection.js
@@ -0,0 +1,91 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ /**
+ * @memberof OCA.Versions
+ */
+ var VersionCollection = OC.Backbone.Collection.extend({
+ model: OCA.Versions.VersionModel,
+
+ /**
+ * @var OCA.Files.FileInfoModel
+ */
+ _fileInfo: null,
+
+ _endReached: false,
+ _currentIndex: 0,
+
+ url: function() {
+ var url = OC.generateUrl('/apps/files_versions/ajax/getVersions.php');
+ var query = {
+ source: this._fileInfo.getFullPath(),
+ start: this._currentIndex
+ };
+ return url + '?' + OC.buildQueryString(query);
+ },
+
+ setFileInfo: function(fileInfo) {
+ this._fileInfo = fileInfo;
+ // reset
+ this._endReached = false;
+ this._currentIndex = 0;
+ },
+
+ getFileInfo: function() {
+ return this._fileInfo;
+ },
+
+ hasMoreResults: function() {
+ return !this._endReached;
+ },
+
+ fetch: function(options) {
+ if (!options || options.remove) {
+ this._currentIndex = 0;
+ }
+ return OC.Backbone.Collection.prototype.fetch.apply(this, arguments);
+ },
+
+ /**
+ * Fetch the next set of results
+ */
+ fetchNext: function() {
+ if (!this.hasMoreResults()) {
+ return null;
+ }
+ if (this._currentIndex === 0) {
+ return this.fetch();
+ }
+ return this.fetch({remove: false});
+ },
+
+ parse: function(result) {
+ var results = _.map(result.data.versions, function(version) {
+ var revision = parseInt(version.version, 10);
+ return {
+ id: revision,
+ name: version.name,
+ fullPath: version.path,
+ timestamp: revision,
+ size: version.size
+ };
+ });
+ this._endReached = result.data.endReached;
+ this._currentIndex += results.length;
+ return results;
+ }
+ });
+
+ OCA.Versions = OCA.Versions || {};
+
+ OCA.Versions.VersionCollection = VersionCollection;
+})();
+
diff --git a/apps/files_versions/js/versionmodel.js b/apps/files_versions/js/versionmodel.js
new file mode 100644
index 00000000000..dc610fc2144
--- /dev/null
+++ b/apps/files_versions/js/versionmodel.js
@@ -0,0 +1,77 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ /**
+ * @memberof OCA.Versions
+ */
+ var VersionModel = OC.Backbone.Model.extend({
+
+ /**
+ * Restores the original file to this revision
+ */
+ revert: function(options) {
+ options = options ? _.clone(options) : {};
+ var model = this;
+ var file = this.getFullPath();
+ var revision = this.get('timestamp');
+
+ $.ajax({
+ type: 'GET',
+ url: OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php'),
+ dataType: 'json',
+ data: {
+ file: file,
+ revision: revision
+ },
+ success: function(response) {
+ if (response.status === 'error') {
+ if (options.error) {
+ options.error.call(options.context, model, response, options);
+ }
+ model.trigger('error', model, response, options);
+ } else {
+ if (options.success) {
+ options.success.call(options.context, model, response, options);
+ }
+ model.trigger('revert', model, response, options);
+ }
+ }
+ });
+ },
+
+ getFullPath: function() {
+ return this.get('fullPath');
+ },
+
+ getPreviewUrl: function() {
+ var url = OC.generateUrl('/apps/files_versions/preview');
+ var params = {
+ file: this.get('fullPath'),
+ version: this.get('timestamp')
+ };
+ return url + '?' + OC.buildQueryString(params);
+ },
+
+ getDownloadUrl: function() {
+ var url = OC.generateUrl('/apps/files_versions/download.php');
+ var params = {
+ file: this.get('fullPath'),
+ revision: this.get('timestamp')
+ };
+ return url + '?' + OC.buildQueryString(params);
+ }
+ });
+
+ OCA.Versions = OCA.Versions || {};
+
+ OCA.Versions.VersionModel = VersionModel;
+})();
+
diff --git a/apps/files_versions/js/versions.js b/apps/files_versions/js/versions.js
deleted file mode 100644
index e86bb4c3307..00000000000
--- a/apps/files_versions/js/versions.js
+++ /dev/null
@@ -1,193 +0,0 @@
-/*
- * Copyright (c) 2014
- *
- * This file is licensed under the Affero General Public License version 3
- * or later.
- *
- * See the COPYING-README file.
- *
- */
-
-/* global scanFiles, escapeHTML, formatDate */
-$(document).ready(function(){
-
- // TODO: namespace all this as OCA.FileVersions
-
- if ($('#isPublic').val()){
- // no versions actions in public mode
- // beware of https://github.com/owncloud/core/issues/4545
- // as enabling this might hang Chrome
- return;
- }
-
- if (OCA.Files) {
- // Add versions button to 'files/index.php'
- OCA.Files.fileActions.register(
- 'file',
- 'Versions',
- OC.PERMISSION_UPDATE,
- function() {
- // Specify icon for hitory button
- return OC.imagePath('core','actions/history');
- }, function(filename, context){
- // Action to perform when clicked
- if (scanFiles.scanning){return;}//workaround to prevent additional http request block scanning feedback
-
- var file = context.dir.replace(/(?!<=\/)$|\/$/, '/' + filename);
- var createDropDown = true;
- // Check if drop down is already visible for a different file
- if (($('#dropdown').length > 0) ) {
- if ( $('#dropdown').hasClass('drop-versions') && file == $('#dropdown').data('file')) {
- createDropDown = false;
- }
- $('#dropdown').slideUp(OC.menuSpeed);
- $('#dropdown').remove();
- $('tr').removeClass('mouseOver');
- }
-
- if(createDropDown === true) {
- createVersionsDropdown(filename, file, context.fileList);
- }
- }, t('files_versions', 'Versions')
- );
- }
-
- $(document).on("click", 'span[class="revertVersion"]', function() {
- var revision = $(this).attr('id');
- var file = $(this).attr('value');
- revertFile(file, revision);
- });
-
-});
-
-function revertFile(file, revision) {
-
- $.ajax({
- type: 'GET',
- url: OC.linkTo('files_versions', 'ajax/rollbackVersion.php'),
- dataType: 'json',
- data: {file: file, revision: revision},
- async: false,
- success: function(response) {
- if (response.status === 'error') {
- OC.Notification.show( t('files_version', 'Failed to revert {file} to revision {timestamp}.', {file:file, timestamp:formatDate(revision * 1000)}) );
- } else {
- $('#dropdown').slideUp(OC.menuSpeed, function() {
- $('#dropdown').closest('tr').find('.modified:first').html(relative_modified_date(revision));
- $('#dropdown').remove();
- $('tr').removeClass('mouseOver');
- });
- }
- }
- });
-
-}
-
-function goToVersionPage(url){
- window.location.assign(url);
-}
-
-function createVersionsDropdown(filename, files, fileList) {
-
- var start = 0;
- var fileEl;
-
- var html = '<div id="dropdown" class="drop drop-versions" data-file="'+escapeHTML(files)+'">';
- html += '<div id="private">';
- html += '<ul id="found_versions">';
- html += '</ul>';
- html += '</div>';
- html += '<input type="button" value="'+ t('files_versions', 'More versions...') + '" name="show-more-versions" id="show-more-versions" style="display: none;" />';
-
- if (filename) {
- fileEl = fileList.findFileEl(filename);
- fileEl.addClass('mouseOver');
- $(html).appendTo(fileEl.find('td.filename'));
- } else {
- $(html).appendTo($('thead .share'));
- }
-
- getVersions(start);
- start = start + 5;
-
- $("#show-more-versions").click(function() {
- //get more versions
- getVersions(start);
- start = start + 5;
- });
-
- function getVersions(start) {
- $.ajax({
- type: 'GET',
- url: OC.filePath('files_versions', 'ajax', 'getVersions.php'),
- dataType: 'json',
- data: {source: files, start: start},
- async: false,
- success: function(result) {
- var versions = result.data.versions;
- if (result.data.endReached === true) {
- $("#show-more-versions").css("display", "none");
- } else {
- $("#show-more-versions").css("display", "block");
- }
- if (versions) {
- $.each(versions, function(index, row) {
- addVersion(row);
- });
- } else {
- $('<div style="text-align:center;">'+ t('files_versions', 'No other versions available') + '</div>').appendTo('#dropdown');
- }
- $('#found_versions').change(function() {
- var revision = parseInt($(this).val());
- revertFile(files, revision);
- });
- }
- });
- }
-
- function addVersion( revision ) {
- var title = formatDate(revision.version*1000);
- var name ='<span class="versionDate" title="' + title + '">' + revision.humanReadableTimestamp + '</span>';
-
- var path = OC.filePath('files_versions', '', 'download.php');
-
- var preview = '<img class="preview" src="'+revision.preview+'"/>';
-
- var download ='<a href="' + path + "?file=" + encodeURIComponent(files) + '&revision=' + revision.version + '">';
- download+='<img';
- download+=' src="' + OC.imagePath('core', 'actions/download') + '"';
- download+=' name="downloadVersion" />';
- download+=name;
- download+='</a>';
-
- var revert='<span class="revertVersion"';
- revert+=' id="' + revision.version + '">';
- revert+='<img';
- revert+=' src="' + OC.imagePath('core', 'actions/history') + '"';
- revert+=' name="revertVersion"';
- revert+='/>'+t('files_versions', 'Restore')+'</span>';
-
- var version=$('<li/>');
- version.attr('value', revision.version);
- version.html(preview + download + revert);
- // add file here for proper name escaping
- version.find('span.revertVersion').attr('value', files);
-
- version.appendTo('#found_versions');
- }
-
- $('#dropdown').slideDown(1000);
-}
-
-$(this).click(
- function(event) {
- if ($('#dropdown').has(event.target).length === 0 && $('#dropdown').hasClass('drop-versions')) {
- $('#dropdown').slideUp(OC.menuSpeed, function() {
- $('#dropdown').remove();
- $('tr').removeClass('mouseOver');
- });
- }
-
-
- }
-);
diff --git a/apps/files_versions/js/versionstabview.js b/apps/files_versions/js/versionstabview.js
new file mode 100644
index 00000000000..1f84428e616
--- /dev/null
+++ b/apps/files_versions/js/versionstabview.js
@@ -0,0 +1,198 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ var TEMPLATE_ITEM =
+ '<li data-revision="{{timestamp}}">' +
+ '<img class="preview" src="{{previewUrl}}"/>' +
+ '<a href="{{downloadUrl}}" class="downloadVersion"><img src="{{downloadIconUrl}}" />' +
+ '<span class="versiondate has-tooltip" title="{{formattedTimestamp}}">{{relativeTimestamp}}</span>' +
+ '</a>' +
+ '<a href="#" class="revertVersion"><img src="{{revertIconUrl}}" />{{revertLabel}}</a>' +
+ '</li>';
+
+ var TEMPLATE =
+ '<ul class="versions"></ul>' +
+ '<div class="clear-float"></div>' +
+ '<div class="empty hidden">{{emptyResultLabel}}</div>' +
+ '<input type="button" class="showMoreVersions hidden" value="{{moreVersionsLabel}}"' +
+ ' name="show-more-versions" id="show-more-versions" />' +
+ '<div class="loading hidden" style="height: 50px"></div>';
+
+ /**
+ * @memberof OCA.Versions
+ */
+ var VersionsTabView = OCA.Files.DetailTabView.extend(
+ /** @lends OCA.Versions.VersionsTabView.prototype */ {
+ id: 'versionsTabView',
+ className: 'tab versionsTabView',
+
+ _template: null,
+
+ $versionsContainer: null,
+
+ events: {
+ 'click .revertVersion': '_onClickRevertVersion',
+ 'click .showMoreVersions': '_onClickShowMoreVersions'
+ },
+
+ initialize: function() {
+ this.collection = new OCA.Versions.VersionCollection();
+ this.collection.on('request', this._onRequest, this);
+ this.collection.on('sync', this._onEndRequest, this);
+ this.collection.on('update', this._onUpdate, this);
+ this.collection.on('error', this._onError, this);
+ this.collection.on('add', this._onAddModel, this);
+ },
+
+ getLabel: function() {
+ return t('files_versions', 'Versions');
+ },
+
+ nextPage: function() {
+ if (this._loading || !this.collection.hasMoreResults()) {
+ return;
+ }
+
+ if (this.collection.getFileInfo() && this.collection.getFileInfo().isDirectory()) {
+ return;
+ }
+ this.collection.fetchNext();
+ },
+
+ _onClickShowMoreVersions: function(ev) {
+ ev.preventDefault();
+ this.nextPage();
+ },
+
+ _onClickRevertVersion: function(ev) {
+ var self = this;
+ var $target = $(ev.target);
+ var fileInfoModel = this.collection.getFileInfo();
+ var revision;
+ if (!$target.is('li')) {
+ $target = $target.closest('li');
+ }
+
+ ev.preventDefault();
+ revision = $target.attr('data-revision');
+
+ var versionModel = this.collection.get(revision);
+ versionModel.revert({
+ success: function() {
+ // reset and re-fetch the updated collection
+ self.collection.setFileInfo(fileInfoModel);
+ self.collection.fetch();
+
+ // update original model
+ fileInfoModel.trigger('busy', fileInfoModel, false);
+ fileInfoModel.set({
+ size: versionModel.get('size'),
+ mtime: versionModel.get('timestamp') * 1000,
+ // temp dummy, until we can do a PROPFIND
+ etag: versionModel.get('id') + versionModel.get('timestamp')
+ });
+ },
+
+ error: function() {
+ OC.Notification.showTemporary(
+ t('files_version', 'Failed to revert {file} to revision {timestamp}.', {
+ file: versionModel.getFullPath(),
+ timestamp: OC.Util.formatDate(versionModel.get('timestamp') * 1000)
+ })
+ );
+ }
+ });
+
+ // spinner
+ this._toggleLoading(true);
+ fileInfoModel.trigger('busy', fileInfoModel, true);
+ },
+
+ _toggleLoading: function(state) {
+ this._loading = state;
+ this.$el.find('.loading').toggleClass('hidden', !state);
+ },
+
+ _onRequest: function() {
+ this._toggleLoading(true);
+ this.$el.find('.showMoreVersions').addClass('hidden');
+ },
+
+ _onEndRequest: function() {
+ this._toggleLoading(false);
+ this.$el.find('.empty').toggleClass('hidden', !!this.collection.length);
+ this.$el.find('.showMoreVersions').toggleClass('hidden', !this.collection.hasMoreResults());
+ },
+
+ _onAddModel: function(model) {
+ this.$versionsContainer.append(this.itemTemplate(this._formatItem(model)));
+ },
+
+ template: function(data) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+
+ return this._template(data);
+ },
+
+ itemTemplate: function(data) {
+ if (!this._itemTemplate) {
+ this._itemTemplate = Handlebars.compile(TEMPLATE_ITEM);
+ }
+
+ return this._itemTemplate(data);
+ },
+
+ setFileInfo: function(fileInfo) {
+ if (fileInfo) {
+ this.render();
+ this.collection.setFileInfo(fileInfo);
+ this.collection.reset({silent: true});
+ this.nextPage();
+ } else {
+ this.render();
+ this.collection.reset();
+ }
+ },
+
+ _formatItem: function(version) {
+ var timestamp = version.get('timestamp') * 1000;
+ return _.extend({
+ formattedTimestamp: OC.Util.formatDate(timestamp),
+ relativeTimestamp: OC.Util.relativeModifiedDate(timestamp),
+ downloadUrl: version.getDownloadUrl(),
+ downloadIconUrl: OC.imagePath('core', 'actions/download'),
+ revertIconUrl: OC.imagePath('core', 'actions/history'),
+ previewUrl: version.getPreviewUrl(),
+ revertLabel: t('files_versions', 'Restore'),
+ }, version.attributes);
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ this.$el.html(this.template({
+ emptyResultLabel: t('files_versions', 'No other versions available'),
+ moreVersionsLabel: t('files_versions', 'More versions...')
+ }));
+ this.$el.find('.has-tooltip').tooltip();
+ this.$versionsContainer = this.$el.find('ul.versions');
+ this.delegateEvents();
+ }
+ });
+
+ OCA.Versions = OCA.Versions || {};
+
+ OCA.Versions.VersionsTabView = VersionsTabView;
+})();
+
diff --git a/apps/files_versions/lib/hooks.php b/apps/files_versions/lib/hooks.php
index ccd89a4a14f..5ef2cc3c7d0 100644
--- a/apps/files_versions/lib/hooks.php
+++ b/apps/files_versions/lib/hooks.php
@@ -43,6 +43,9 @@ class Hooks {
\OCP\Util::connectHook('OC_Filesystem', 'post_copy', 'OCA\Files_Versions\Hooks', 'copy_hook');
\OCP\Util::connectHook('OC_Filesystem', 'rename', 'OCA\Files_Versions\Hooks', 'pre_renameOrCopy_hook');
\OCP\Util::connectHook('OC_Filesystem', 'copy', 'OCA\Files_Versions\Hooks', 'pre_renameOrCopy_hook');
+
+ $eventDispatcher = \OC::$server->getEventDispatcher();
+ $eventDispatcher->addListener('OCA\Files::loadAdditionalScripts', ['OCA\Files_Versions\Hooks', 'onLoadFilesAppScripts']);
}
/**
@@ -154,4 +157,13 @@ class Hooks {
}
}
+ /**
+ * Load additional scripts when the files app is visible
+ */
+ public static function onLoadFilesAppScripts() {
+ \OCP\Util::addScript('files_versions', 'versionmodel');
+ \OCP\Util::addScript('files_versions', 'versioncollection');
+ \OCP\Util::addScript('files_versions', 'versionstabview');
+ \OCP\Util::addScript('files_versions', 'filesplugin');
+ }
}
diff --git a/apps/files_versions/tests/js/versioncollectionSpec.js b/apps/files_versions/tests/js/versioncollectionSpec.js
new file mode 100644
index 00000000000..87065fa1d36
--- /dev/null
+++ b/apps/files_versions/tests/js/versioncollectionSpec.js
@@ -0,0 +1,161 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+describe('OCA.Versions.VersionCollection', function() {
+ var VersionCollection = OCA.Versions.VersionCollection;
+ var collection, fileInfoModel;
+
+ beforeEach(function() {
+ fileInfoModel = new OCA.Files.FileInfoModel({
+ path: '/subdir',
+ name: 'some file.txt'
+ });
+ collection = new VersionCollection();
+ collection.setFileInfo(fileInfoModel);
+ });
+ it('fetches the next page', function() {
+ collection.fetchNext();
+
+ expect(fakeServer.requests.length).toEqual(1);
+ expect(fakeServer.requests[0].url).toEqual(
+ OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
+ '?source=%2Fsubdir%2Fsome%20file.txt&start=0'
+ );
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'success',
+ data: {
+ endReached: false,
+ versions: [{
+ version: 10000000,
+ size: 123,
+ name: 'some file.txt',
+ fullPath: '/subdir/some file.txt'
+ },{
+ version: 15000000,
+ size: 150,
+ name: 'some file.txt',
+ path: '/subdir/some file.txt'
+ }]
+ }
+ })
+ );
+
+ expect(collection.length).toEqual(2);
+ expect(collection.hasMoreResults()).toEqual(true);
+
+ collection.fetchNext();
+
+ expect(fakeServer.requests.length).toEqual(2);
+ expect(fakeServer.requests[1].url).toEqual(
+ OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
+ '?source=%2Fsubdir%2Fsome%20file.txt&start=2'
+ );
+ fakeServer.requests[1].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'success',
+ data: {
+ endReached: true,
+ versions: [{
+ version: 18000000,
+ size: 123,
+ name: 'some file.txt',
+ path: '/subdir/some file.txt'
+ }]
+ }
+ })
+ );
+
+ expect(collection.length).toEqual(3);
+ expect(collection.hasMoreResults()).toEqual(false);
+
+ collection.fetchNext();
+
+ // no further requests
+ expect(fakeServer.requests.length).toEqual(2);
+ });
+ it('properly parses the results', function() {
+ collection.fetchNext();
+
+ expect(fakeServer.requests.length).toEqual(1);
+ expect(fakeServer.requests[0].url).toEqual(
+ OC.generateUrl('apps/files_versions/ajax/getVersions.php') +
+ '?source=%2Fsubdir%2Fsome%20file.txt&start=0'
+ );
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'success',
+ data: {
+ endReached: false,
+ versions: [{
+ version: 10000000,
+ size: 123,
+ name: 'some file.txt',
+ path: '/subdir/some file.txt'
+ },{
+ version: 15000000,
+ size: 150,
+ name: 'some file.txt',
+ path: '/subdir/some file.txt'
+ }]
+ }
+ })
+ );
+
+ expect(collection.length).toEqual(2);
+
+ var model = collection.at(0);
+ expect(model.get('id')).toEqual(10000000);
+ expect(model.get('timestamp')).toEqual(10000000);
+ expect(model.get('name')).toEqual('some file.txt');
+ expect(model.get('fullPath')).toEqual('/subdir/some file.txt');
+ expect(model.get('size')).toEqual(123);
+
+ model = collection.at(1);
+ expect(model.get('id')).toEqual(15000000);
+ expect(model.get('timestamp')).toEqual(15000000);
+ expect(model.get('name')).toEqual('some file.txt');
+ expect(model.get('fullPath')).toEqual('/subdir/some file.txt');
+ expect(model.get('size')).toEqual(150);
+ });
+ it('resets page counted when setting a new file info model', function() {
+ collection.fetchNext();
+
+ expect(fakeServer.requests.length).toEqual(1);
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'success',
+ data: {
+ endReached: true,
+ versions: [{
+ version: 18000000,
+ size: 123,
+ name: 'some file.txt',
+ path: '/subdir/some file.txt'
+ }]
+ }
+ })
+ );
+
+ expect(collection.hasMoreResults()).toEqual(false);
+
+ collection.setFileInfo(fileInfoModel);
+
+ expect(collection.hasMoreResults()).toEqual(true);
+ });
+});
+
diff --git a/apps/files_versions/tests/js/versionmodelSpec.js b/apps/files_versions/tests/js/versionmodelSpec.js
new file mode 100644
index 00000000000..0f1c06581d5
--- /dev/null
+++ b/apps/files_versions/tests/js/versionmodelSpec.js
@@ -0,0 +1,96 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+describe('OCA.Versions.VersionModel', function() {
+ var VersionModel = OCA.Versions.VersionModel;
+ var model;
+
+ beforeEach(function() {
+ model = new VersionModel({
+ id: 10000000,
+ timestamp: 10000000,
+ fullPath: '/subdir/some file.txt',
+ name: 'some file.txt',
+ size: 150
+ });
+ });
+
+ it('returns the full path', function() {
+ expect(model.getFullPath()).toEqual('/subdir/some file.txt');
+ });
+ it('returns the preview url', function() {
+ expect(model.getPreviewUrl())
+ .toEqual(OC.generateUrl('/apps/files_versions/preview') +
+ '?file=%2Fsubdir%2Fsome%20file.txt&version=10000000'
+ );
+ });
+ it('returns the download url', function() {
+ expect(model.getDownloadUrl())
+ .toEqual(OC.generateUrl('/apps/files_versions/download.php') +
+ '?file=%2Fsubdir%2Fsome%20file.txt&revision=10000000'
+ );
+ });
+ describe('reverting', function() {
+ var revertEventStub;
+ var successStub;
+ var errorStub;
+
+ beforeEach(function() {
+ revertEventStub = sinon.stub();
+ errorStub = sinon.stub();
+ successStub = sinon.stub();
+
+ model.on('revert', revertEventStub);
+ model.on('error', errorStub);
+ });
+ it('tells the server to revert when calling the revert method', function() {
+ model.revert({
+ success: successStub
+ });
+
+ expect(fakeServer.requests.length).toEqual(1);
+ expect(fakeServer.requests[0].url)
+ .toEqual(
+ OC.generateUrl('/apps/files_versions/ajax/rollbackVersion.php') +
+ '?file=%2Fsubdir%2Fsome+file.txt&revision=10000000'
+ );
+
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'success',
+ })
+ );
+
+ expect(revertEventStub.calledOnce).toEqual(true);
+ expect(successStub.calledOnce).toEqual(true);
+ expect(errorStub.notCalled).toEqual(true);
+ });
+ it('triggers error event when server returns a failure', function() {
+ model.revert({
+ success: successStub
+ });
+
+ expect(fakeServer.requests.length).toEqual(1);
+ fakeServer.requests[0].respond(
+ 200,
+ { 'Content-Type': 'application/json' },
+ JSON.stringify({
+ status: 'error',
+ })
+ );
+
+ expect(revertEventStub.notCalled).toEqual(true);
+ expect(successStub.notCalled).toEqual(true);
+ expect(errorStub.calledOnce).toEqual(true);
+ });
+ });
+});
+
diff --git a/apps/files_versions/tests/js/versionstabviewSpec.js b/apps/files_versions/tests/js/versionstabviewSpec.js
new file mode 100644
index 00000000000..4435f38ef7e
--- /dev/null
+++ b/apps/files_versions/tests/js/versionstabviewSpec.js
@@ -0,0 +1,208 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+describe('OCA.Versions.VersionsTabView', function() {
+ var VersionCollection = OCA.Versions.VersionCollection;
+ var VersionModel = OCA.Versions.VersionModel;
+ var VersionsTabView = OCA.Versions.VersionsTabView;
+
+ var fetchStub, fileInfoModel, tabView, testVersions, clock;
+
+ beforeEach(function() {
+ clock = sinon.useFakeTimers(Date.UTC(2015, 6, 17, 1, 2, 0, 3));
+ var time1 = Date.UTC(2015, 6, 17, 1, 2, 0, 3) / 1000;
+ var time2 = Date.UTC(2015, 6, 15, 1, 2, 0, 3) / 1000;
+
+ var version1 = new VersionModel({
+ id: time1,
+ timestamp: time1,
+ name: 'some file.txt',
+ size: 140,
+ fullPath: '/subdir/some file.txt'
+ });
+ var version2 = new VersionModel({
+ id: time2,
+ timestamp: time2,
+ name: 'some file.txt',
+ size: 150,
+ fullPath: '/subdir/some file.txt'
+ });
+
+ testVersions = [version1, version2];
+
+ fetchStub = sinon.stub(VersionCollection.prototype, 'fetch');
+ fileInfoModel = new OCA.Files.FileInfoModel({
+ id: 123,
+ name: 'test.txt'
+ });
+ tabView = new VersionsTabView();
+ tabView.render();
+ });
+
+ afterEach(function() {
+ fetchStub.restore();
+ tabView.remove();
+ clock.restore();
+ });
+
+ describe('rendering', function() {
+ it('reloads matching versions when setting file info model', function() {
+ tabView.setFileInfo(fileInfoModel);
+ expect(fetchStub.calledOnce).toEqual(true);
+ });
+
+ it('renders loading icon while fetching versions', function() {
+ tabView.setFileInfo(fileInfoModel);
+ tabView.collection.trigger('request');
+
+ expect(tabView.$el.find('.loading').length).toEqual(1);
+ expect(tabView.$el.find('.versions li').length).toEqual(0);
+ });
+
+ it('renders versions', function() {
+
+ tabView.setFileInfo(fileInfoModel);
+ tabView.collection.set(testVersions);
+
+ var version1 = testVersions[0];
+ var version2 = testVersions[1];
+ var $versions = tabView.$el.find('.versions>li');
+ expect($versions.length).toEqual(2);
+ var $item = $versions.eq(0);
+ expect($item.find('.downloadVersion').attr('href')).toEqual(version1.getDownloadUrl());
+ expect($item.find('.versiondate').text()).toEqual('a few seconds ago');
+ expect($item.find('.revertVersion').length).toEqual(1);
+ expect($item.find('.preview').attr('src')).toEqual(version1.getPreviewUrl());
+
+ $item = $versions.eq(1);
+ expect($item.find('.downloadVersion').attr('href')).toEqual(version2.getDownloadUrl());
+ expect($item.find('.versiondate').text()).toEqual('2 days ago');
+ expect($item.find('.revertVersion').length).toEqual(1);
+ expect($item.find('.preview').attr('src')).toEqual(version2.getPreviewUrl());
+ });
+ });
+
+ describe('More versions', function() {
+ var hasMoreResultsStub;
+
+ beforeEach(function() {
+ tabView.collection.set(testVersions);
+ hasMoreResultsStub = sinon.stub(VersionCollection.prototype, 'hasMoreResults');
+ });
+ afterEach(function() {
+ hasMoreResultsStub.restore();
+ });
+
+ it('shows "More versions" button when more versions are available', function() {
+ hasMoreResultsStub.returns(true);
+ tabView.collection.trigger('sync');
+
+ expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(false);
+ });
+ it('does not show "More versions" button when more versions are available', function() {
+ hasMoreResultsStub.returns(false);
+ tabView.collection.trigger('sync');
+
+ expect(tabView.$el.find('.showMoreVersions').hasClass('hidden')).toEqual(true);
+ });
+ it('fetches and appends the next page when clicking the "More" button', function() {
+ hasMoreResultsStub.returns(true);
+
+ expect(fetchStub.notCalled).toEqual(true);
+
+ tabView.$el.find('.showMoreVersions').click();
+
+ expect(fetchStub.calledOnce).toEqual(true);
+ });
+ it('appends version to the list when added to collection', function() {
+ var time3 = Date.UTC(2015, 6, 10, 1, 0, 0, 0) / 1000;
+
+ var version3 = new VersionModel({
+ id: time3,
+ timestamp: time3,
+ name: 'some file.txt',
+ size: 54,
+ fullPath: '/subdir/some file.txt'
+ });
+
+ tabView.collection.add(version3);
+
+ expect(tabView.$el.find('.versions>li').length).toEqual(3);
+
+ var $item = tabView.$el.find('.versions>li').eq(2);
+ expect($item.find('.downloadVersion').attr('href')).toEqual(version3.getDownloadUrl());
+ expect($item.find('.versiondate').text()).toEqual('7 days ago');
+ expect($item.find('.revertVersion').length).toEqual(1);
+ expect($item.find('.preview').attr('src')).toEqual(version3.getPreviewUrl());
+ });
+ });
+
+ describe('Reverting', function() {
+ var revertStub;
+
+ beforeEach(function() {
+ revertStub = sinon.stub(VersionModel.prototype, 'revert');
+ tabView.setFileInfo(fileInfoModel);
+ tabView.collection.set(testVersions);
+ });
+
+ afterEach(function() {
+ revertStub.restore();
+ });
+
+ it('tells the model to revert when clicking "Revert"', function() {
+ tabView.$el.find('.revertVersion').eq(1).click();
+
+ expect(revertStub.calledOnce).toEqual(true);
+ });
+ it('triggers busy state during revert', function() {
+ var busyStub = sinon.stub();
+ fileInfoModel.on('busy', busyStub);
+
+ tabView.$el.find('.revertVersion').eq(1).click();
+
+ expect(busyStub.calledOnce).toEqual(true);
+ expect(busyStub.calledWith(fileInfoModel, true)).toEqual(true);
+
+ busyStub.reset();
+ revertStub.getCall(0).args[0].success();
+
+ expect(busyStub.calledOnce).toEqual(true);
+ expect(busyStub.calledWith(fileInfoModel, false)).toEqual(true);
+ });
+ it('updates the file info model with the information from the reverted revision', function() {
+ var changeStub = sinon.stub();
+ fileInfoModel.on('change', changeStub);
+
+ tabView.$el.find('.revertVersion').eq(1).click();
+
+ expect(changeStub.notCalled).toEqual(true);
+
+ revertStub.getCall(0).args[0].success();
+
+ expect(changeStub.calledOnce).toEqual(true);
+ var changes = changeStub.getCall(0).args[0].changed;
+ expect(changes.size).toEqual(150);
+ expect(changes.mtime).toEqual(testVersions[1].get('timestamp') * 1000);
+ expect(changes.etag).toBeDefined();
+ });
+ it('shows notification on revert error', function() {
+ var notificationStub = sinon.stub(OC.Notification, 'showTemporary');
+
+ tabView.$el.find('.revertVersion').eq(1).click();
+
+ revertStub.getCall(0).args[0].error();
+
+ expect(notificationStub.calledOnce).toEqual(true);
+
+ notificationStub.restore();
+ });
+ });
+});
+