diff options
author | Vincent Petry <pvince81@owncloud.com> | 2015-09-01 19:29:55 +0200 |
---|---|---|
committer | Vincent Petry <pvince81@owncloud.com> | 2015-09-03 16:47:24 +0200 |
commit | 310d79728447ecf69f18d0b61a527397bd961888 (patch) | |
tree | 805b2a0a40ed5ce7acb58afb90ad7c18e760e037 /apps | |
parent | e9e42fff61a922f11a3b1014d810562537950b6a (diff) | |
download | nextcloud-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.js | 2 | ||||
-rw-r--r-- | apps/files/js/detailtabview.js | 7 | ||||
-rw-r--r-- | apps/files/js/filelist.js | 40 | ||||
-rw-r--r-- | apps/files/js/tagsplugin.js | 16 | ||||
-rw-r--r-- | apps/files/tests/js/tagspluginspec.js | 18 | ||||
-rw-r--r-- | apps/files_versions/ajax/getVersions.php | 2 | ||||
-rw-r--r-- | apps/files_versions/ajax/preview.php | 5 | ||||
-rw-r--r-- | apps/files_versions/css/versions.css | 27 | ||||
-rw-r--r-- | apps/files_versions/js/filesplugin.js | 34 | ||||
-rw-r--r-- | apps/files_versions/js/versioncollection.js | 91 | ||||
-rw-r--r-- | apps/files_versions/js/versionmodel.js | 77 | ||||
-rw-r--r-- | apps/files_versions/js/versions.js | 193 | ||||
-rw-r--r-- | apps/files_versions/js/versionstabview.js | 198 | ||||
-rw-r--r-- | apps/files_versions/lib/hooks.php | 12 | ||||
-rw-r--r-- | apps/files_versions/tests/js/versioncollectionSpec.js | 161 | ||||
-rw-r--r-- | apps/files_versions/tests/js/versionmodelSpec.js | 96 | ||||
-rw-r--r-- | apps/files_versions/tests/js/versionstabviewSpec.js | 208 |
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(); + }); + }); +}); + |