diff options
-rw-r--r-- | apps/systemtags/appinfo/app.php | 14 | ||||
-rw-r--r-- | apps/systemtags/css/systemtagsfilelist.css | 29 | ||||
-rw-r--r-- | apps/systemtags/img/tag.png | bin | 0 -> 293 bytes | |||
-rw-r--r-- | apps/systemtags/img/tag.svg | 5 | ||||
-rw-r--r-- | apps/systemtags/js/app.js | 87 | ||||
-rw-r--r-- | apps/systemtags/js/filesplugin.js | 3 | ||||
-rw-r--r-- | apps/systemtags/js/systemtagsfilelist.js | 240 | ||||
-rw-r--r-- | apps/systemtags/list.php | 25 | ||||
-rw-r--r-- | apps/systemtags/templates/list.php | 38 | ||||
-rw-r--r-- | apps/systemtags/tests/js/systemtagsfilelistSpec.js | 226 | ||||
-rw-r--r-- | core/js/files/client.js | 73 | ||||
-rw-r--r-- | core/js/systemtags/systemtagsinputfield.js | 4 | ||||
-rw-r--r-- | core/js/tests/specs/files/clientSpec.js | 154 | ||||
-rw-r--r-- | tests/karma.config.js | 1 |
14 files changed, 897 insertions, 2 deletions
diff --git a/apps/systemtags/appinfo/app.php b/apps/systemtags/appinfo/app.php index 0bb57e1227b..6bcbae4d0da 100644 --- a/apps/systemtags/appinfo/app.php +++ b/apps/systemtags/appinfo/app.php @@ -39,9 +39,11 @@ $eventDispatcher->addListener( \OCP\Util::addScript('systemtags/systemtagscollection'); \OCP\Util::addScript('systemtags/systemtagsinputfield'); \OCP\Util::addScript('systemtags', 'app'); + \OCP\Util::addScript('systemtags', 'systemtagsfilelist'); \OCP\Util::addScript('systemtags', 'filesplugin'); \OCP\Util::addScript('systemtags', 'systemtagsinfoview'); \OCP\Util::addStyle('systemtags'); + \OCP\Util::addStyle('systemtags', 'systemtagsfilelist'); } ); @@ -73,3 +75,15 @@ $mapperListener = function(MapperEvent $event) use ($activityManager) { $eventDispatcher->addListener(MapperEvent::EVENT_ASSIGN, $mapperListener); $eventDispatcher->addListener(MapperEvent::EVENT_UNASSIGN, $mapperListener); + +$l = \OC::$server->getL10N('files_sharing'); + +\OCA\Files\App::getNavigationManager()->add( + array( + 'id' => 'systemtagsfilter', + 'appname' => 'systemtags', + 'script' => 'list.php', + 'order' => 9, + 'name' => $l->t('Tags') + ) +); diff --git a/apps/systemtags/css/systemtagsfilelist.css b/apps/systemtags/css/systemtagsfilelist.css new file mode 100644 index 00000000000..e8fb665e26b --- /dev/null +++ b/apps/systemtags/css/systemtagsfilelist.css @@ -0,0 +1,29 @@ +/* + * Copyright (c) 2016 + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +#app-content-systemtagsfilter .select2-container { + width: 30%; +} + +#app-content-systemtagsfilter .select2-choices { + white-space: nowrap; + text-overflow: ellipsis; + background: #fff; + color: #555; + box-sizing: content-box; + border-radius: 3px; + border: 1px solid #ddd; + margin: 3px 3px 3px 0; + padding: 0; + min-height: auto; +} + +.nav-icon-systemtagsfilter { + background-image: url('../img/tag.svg'); +} diff --git a/apps/systemtags/img/tag.png b/apps/systemtags/img/tag.png Binary files differnew file mode 100644 index 00000000000..5f4767a6f46 --- /dev/null +++ b/apps/systemtags/img/tag.png diff --git a/apps/systemtags/img/tag.svg b/apps/systemtags/img/tag.svg new file mode 100644 index 00000000000..6024607dd0a --- /dev/null +++ b/apps/systemtags/img/tag.svg @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="UTF-8" standalone="no"?> +<svg xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#" xmlns="http://www.w3.org/2000/svg" height="16" width="16" version="1.0" xmlns:cc="http://creativecommons.org/ns#" xmlns:dc="http://purl.org/dc/elements/1.1/"> + <rect style="color:#000000" fill-opacity="0" height="97.986" width="163.31" y="-32.993" x="-62.897"/> + <path opacity=".5" style="color:#000000" d="m6 1c-2.7614 0-5 2.2386-5 5s2.2386 5 5 5c0.98478 0 1.8823-0.28967 2.6562-0.78125l4.4688 4.625c0.09558 0.10527 0.22619 0.16452 0.375 0.15625 0.14882-0.0083 0.3031-0.07119 0.40625-0.1875l0.9375-1.0625c0.19194-0.22089 0.19549-0.53592 0-0.71875l-4.594-4.406c0.478-0.7663 0.75-1.6555 0.75-2.625 0-2.7614-2.2386-5-5-5zm0 2c1.6569 0 3 1.3431 3 3s-1.3431 3-3 3-3-1.3431-3-3 1.3431-3 3-3z"/> +</svg> diff --git a/apps/systemtags/js/app.js b/apps/systemtags/js/app.js index f55aa5c9a6e..d28514358c1 100644 --- a/apps/systemtags/js/app.js +++ b/apps/systemtags/js/app.js @@ -16,5 +16,92 @@ OCA.SystemTags = {}; } + OCA.SystemTags.App = { + + initFileList: function($el) { + if (this._fileList) { + return this._fileList; + } + + this._fileList = new OCA.SystemTags.FileList( + $el, + { + id: 'systemtags', + scrollContainer: $('#app-content'), + fileActions: this._createFileActions() + } + ); + + this._fileList.appName = t('systemtags', 'Tags'); + return this._fileList; + }, + + removeFileList: function() { + if (this._fileList) { + this._fileList.$fileList.empty(); + } + }, + + _createFileActions: function() { + // inherit file actions from the files app + var fileActions = new OCA.Files.FileActions(); + // note: not merging the legacy actions because legacy apps are not + // compatible with the sharing overview and need to be adapted first + fileActions.registerDefaultActions(); + fileActions.merge(OCA.Files.fileActions); + + if (!this._globalActionsInitialized) { + // in case actions are registered later + this._onActionsUpdated = _.bind(this._onActionsUpdated, this); + OCA.Files.fileActions.on('setDefault.app-systemtags', this._onActionsUpdated); + OCA.Files.fileActions.on('registerAction.app-systemtags', this._onActionsUpdated); + this._globalActionsInitialized = true; + } + + // when the user clicks on a folder, redirect to the corresponding + // folder in the files app instead of opening it directly + fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) { + OCA.Files.App.setActiveView('files', {silent: true}); + OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true); + }); + fileActions.setDefault('dir', 'Open'); + return fileActions; + }, + + _onActionsUpdated: function(ev) { + if (!this._fileList) { + return; + } + + if (ev.action) { + this._fileList.fileActions.registerAction(ev.action); + } else if (ev.defaultAction) { + this._fileList.fileActions.setDefault( + ev.defaultAction.mime, + ev.defaultAction.name + ); + } + }, + + /** + * Destroy the app + */ + destroy: function() { + OCA.Files.fileActions.off('setDefault.app-systemtags', this._onActionsUpdated); + OCA.Files.fileActions.off('registerAction.app-systemtags', this._onActionsUpdated); + this.removeFileList(); + this._fileList = null; + delete this._globalActionsInitialized; + } + }; + })(); +$(document).ready(function() { + $('#app-content-systemtagsfilter').on('show', function(e) { + OCA.SystemTags.App.initFileList($(e.target)); + }); + $('#app-content-systemtagsfilter').on('hide', function() { + OCA.SystemTags.App.removeFileList(); + }); +}); diff --git a/apps/systemtags/js/filesplugin.js b/apps/systemtags/js/filesplugin.js index 471440c2e09..588037455ae 100644 --- a/apps/systemtags/js/filesplugin.js +++ b/apps/systemtags/js/filesplugin.js @@ -23,7 +23,8 @@ OCA.SystemTags.FilesPlugin = { allowedLists: [ 'files', - 'favorites' + 'favorites', + 'systemtagsfilter' ], attach: function(fileList) { diff --git a/apps/systemtags/js/systemtagsfilelist.js b/apps/systemtags/js/systemtagsfilelist.js new file mode 100644 index 00000000000..56838018a2c --- /dev/null +++ b/apps/systemtags/js/systemtagsfilelist.js @@ -0,0 +1,240 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ +(function() { + /** + * @class OCA.SystemTags.FileList + * @augments OCA.Files.FileList + * + * @classdesc SystemTags file list. + * Contains a list of files filtered by system tags. + * + * @param $el container element with existing markup for the #controls + * and a table + * @param [options] map of options, see other parameters + * @param {Array.<string>} [options.systemTagIds] array of system tag ids to + * filter by + */ + var FileList = function($el, options) { + this.initialize($el, options); + }; + FileList.prototype = _.extend({}, OCA.Files.FileList.prototype, + /** @lends OCA.SystemTags.FileList.prototype */ { + id: 'systemtagsfilter', + appName: t('systemtags', 'Tagged files'), + + /** + * Array of system tag ids to filter by + * + * @type Array.<string> + */ + _systemTagIds: [], + + _clientSideSort: true, + _allowSelection: false, + + _filterField: null, + + /** + * @private + */ + initialize: function($el, options) { + OCA.Files.FileList.prototype.initialize.apply(this, arguments); + if (this.initialized) { + return; + } + + if (options && options.systemTagIds) { + this._systemTagIds = options.systemTagIds; + } + + OC.Plugins.attach('OCA.SystemTags.FileList', this); + + var $controls = this.$el.find('#controls').empty(); + + this._initFilterField($controls); + }, + + destroy: function() { + this.$filterField.remove(); + + OCA.Files.FileList.prototype.destroy.apply(this, arguments); + }, + + _initFilterField: function($container) { + this.$filterField = $('<input type="hidden" name="tags"/>'); + $container.append(this.$filterField); + this.$filterField.select2({ + placeholder: t('systemtags', 'Select tags to filter by'), + allowClear: false, + multiple: true, + separator: ',', + query: _.bind(this._queryTagsAutocomplete, this), + + id: function(tag) { + return tag.id; + }, + + initSelection: function(element, callback) { + var val = $(element).val().trim(); + if (val) { + var tagIds = val.split(','), + tags = []; + + OC.SystemTags.collection.fetch({ + success: function() { + _.each(tagIds, function(tagId) { + var tag = OC.SystemTags.collection.get(tagId); + if (!_.isUndefined(tag)) { + tags.push(tag.toJSON()); + } + }); + + callback(tags); + } + }); + } else { + callback([]); + } + }, + + formatResult: function (tag) { + return OC.SystemTags.getDescriptiveTag(tag); + }, + + formatSelection: function (tag) { + return OC.SystemTags.getDescriptiveTag(tag)[0].outerHTML; + }, + + escapeMarkup: function(m) { + // prevent double markup escape + return m; + } + }); + this.$filterField.on('change', _.bind(this._onTagsChanged, this)); + return this.$filterField; + }, + + /** + * Autocomplete function for dropdown results + * + * @param {Object} query select2 query object + */ + _queryTagsAutocomplete: function(query) { + OC.SystemTags.collection.fetch({ + success: function() { + var results = OC.SystemTags.collection.filterByName(query.term); + + query.callback({ + results: _.invoke(results, 'toJSON') + }); + } + }); + }, + + /** + * Event handler for when the URL changed + */ + _onUrlChanged: function(e) { + if (e.dir) { + var tags = _.filter(e.dir.split('/'), function(val) { return val.trim() !== ''; }); + this.$filterField.select2('val', tags || []); + this._systemTagIds = tags; + this.reload(); + } + }, + + _onTagsChanged: function(ev) { + var val = $(ev.target).val().trim(); + if (val !== '') { + this._systemTagIds = val.split(','); + } else { + this._systemTagIds = []; + } + + this.$el.trigger(jQuery.Event('changeDirectory', { + dir: this._systemTagIds.join('/') + })); + this.reload(); + }, + + updateEmptyContent: function() { + var dir = this.getCurrentDirectory(); + if (dir === '/') { + // root has special permissions + if (!this._systemTagIds.length) { + // no tags selected + this.$el.find('#emptycontent').html('<div class="icon-systemtags"></div>' + + '<h2>' + t('systemtags', 'Please select tags to filter by') + '</h2>'); + } else { + // tags selected but no results + this.$el.find('#emptycontent').html('<div class="icon-systemtags"></div>' + + '<h2>' + t('systemtags', 'No files found for the selected tags') + '</h2>'); + } + this.$el.find('#emptycontent').toggleClass('hidden', !this.isEmpty); + this.$el.find('#filestable thead th').toggleClass('hidden', this.isEmpty); + } + else { + OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments); + } + }, + + getDirectoryPermissions: function() { + return OC.PERMISSION_READ | OC.PERMISSION_DELETE; + }, + + updateStorageStatistics: function() { + // no op because it doesn't have + // storage info like free space / used space + }, + + reload: function() { + if (!this._systemTagIds.length) { + // don't reload + this.updateEmptyContent(); + this.setFiles([]); + return $.Deferred().resolve(); + } + + this._selectedFiles = {}; + this._selectionSummary.clear(); + if (this._currentFileModel) { + this._currentFileModel.off(); + } + this._currentFileModel = null; + this.$el.find('.select-all').prop('checked', false); + this.showMask(); + this._reloadCall = this.filesClient.getFilteredFiles( + { + systemTagIds: this._systemTagIds + }, + { + properties: this._getWebdavProperties() + } + ); + if (this._detailsView) { + // close sidebar + this._updateDetailsView(null); + } + var callBack = this.reloadCallback.bind(this); + return this._reloadCall.then(callBack, callBack); + }, + + reloadCallback: function(status, result) { + if (result) { + // prepend empty dir info because original handler + result.unshift({}); + } + + return OCA.Files.FileList.prototype.reloadCallback.call(this, status, result); + } + }); + + OCA.SystemTags.FileList = FileList; +})(); diff --git a/apps/systemtags/list.php b/apps/systemtags/list.php new file mode 100644 index 00000000000..dd4fe01e767 --- /dev/null +++ b/apps/systemtags/list.php @@ -0,0 +1,25 @@ +<?php +/** + * @author Vincent Petry <pvince81@owncloud.com> + * + * @copyright Copyright (c) 2016, ownCloud, Inc. + * @license AGPL-3.0 + * + * This code is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License, version 3, + * as published by the Free Software Foundation. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License, version 3, + * along with this program. If not, see <http://www.gnu.org/licenses/> + * + */ +// Check if we are a user +OCP\User::checkLoggedIn(); + +$tmpl = new OCP\Template('systemtags', 'list', ''); +$tmpl->printPage(); diff --git a/apps/systemtags/templates/list.php b/apps/systemtags/templates/list.php new file mode 100644 index 00000000000..841ce7b5b6d --- /dev/null +++ b/apps/systemtags/templates/list.php @@ -0,0 +1,38 @@ +<div id="controls"> +</div> + +<div id="emptycontent" class="hidden"> + <div class="icon-folder"></div> + <h2><?php p($l->t('No files in here')); ?></h2> + <p class="uploadmessage hidden"></p> +</div> + +<div class="nofilterresults emptycontent hidden"> + <div class="icon-search"></div> + <h2><?php p($l->t('No entries found in this folder')); ?></h2> + <p></p> +</div> + +<table id="filestable" data-preview-x="32" data-preview-y="32"> + <thead> + <tr> + <th id='headerName' class="hidden column-name"> + <div id="headerName-container"> + <a class="name sort columntitle" data-sort="name"><span><?php p($l->t( 'Name' )); ?></span><span class="sort-indicator"></span></a> + </div> + </th> + <th id="headerSize" class="hidden column-size"> + <a class="size sort columntitle" data-sort="size"><span><?php p($l->t('Size')); ?></span><span class="sort-indicator"></span></a> + </th> + <th id="headerDate" class="hidden column-mtime"> + <a id="modified" class="columntitle" data-sort="mtime"><span><?php p($l->t( 'Modified' )); ?></span><span class="sort-indicator"></span></a> + </th> + </tr> + </thead> + <tbody id="fileList"> + </tbody> + <tfoot> + </tfoot> +</table> +<input type="hidden" name="dir" id="dir" value="" /> + diff --git a/apps/systemtags/tests/js/systemtagsfilelistSpec.js b/apps/systemtags/tests/js/systemtagsfilelistSpec.js new file mode 100644 index 00000000000..ba41d347ca4 --- /dev/null +++ b/apps/systemtags/tests/js/systemtagsfilelistSpec.js @@ -0,0 +1,226 @@ +/* + * Copyright (c) 2016 Vincent Petry <pvince81@owncloud.com> + * + * This file is licensed under the Affero General Public License version 3 + * or later. + * + * See the COPYING-README file. + * + */ + +describe('OCA.SystemTags.FileList tests', function() { + var FileInfo = OC.Files.FileInfo; + var fileList; + + beforeEach(function() { + // init parameters and test table elements + $('#testArea').append( + '<div id="app-content-container">' + + // init horrible parameters + '<input type="hidden" id="dir" value="/"></input>' + + '<input type="hidden" id="permissions" value="31"></input>' + + '<div id="controls"></div>' + + // dummy table + // TODO: at some point this will be rendered by the fileList class itself! + '<table id="filestable">' + + '<thead><tr>' + + '<th id="headerName" class="hidden column-name">' + + '<input type="checkbox" id="select_all_files" class="select-all">' + + '<a class="name columntitle" data-sort="name"><span>Name</span><span class="sort-indicator"></span></a>' + + '<span class="selectedActions hidden">' + + '</th>' + + '<th class="hidden column-mtime">' + + '<a class="columntitle" data-sort="mtime"><span class="sort-indicator"></span></a>' + + '</th>' + + '</tr></thead>' + + '<tbody id="fileList"></tbody>' + + '<tfoot></tfoot>' + + '</table>' + + '<div id="emptycontent">Empty content message</div>' + + '</div>' + ); + }); + afterEach(function() { + fileList.destroy(); + fileList = undefined; + }); + + describe('filter field', function() { + var select2Stub, oldCollection, fetchTagsStub; + var $tagsField; + + beforeEach(function() { + fetchTagsStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch'); + select2Stub = sinon.stub($.fn, 'select2'); + oldCollection = OC.SystemTags.collection; + OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection([ + { + id: '123', + name: 'abc' + }, + { + id: '456', + name: 'def' + } + ]); + + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: [] + } + ); + $tagsField = fileList.$el.find('[name=tags]'); + }); + afterEach(function() { + select2Stub.restore(); + fetchTagsStub.restore(); + OC.SystemTags.collection = oldCollection; + }); + it('inits select2 on filter field', function() { + expect(select2Stub.calledOnce).toEqual(true); + }); + it('uses global system tags collection', function() { + var callback = sinon.stub(); + var opts = select2Stub.firstCall.args[0]; + + $tagsField.val('123'); + + opts.initSelection($tagsField, callback); + + expect(callback.notCalled).toEqual(true); + expect(fetchTagsStub.calledOnce).toEqual(true); + + fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); + + expect(callback.calledOnce).toEqual(true); + expect(callback.lastCall.args[0]).toEqual([ + OC.SystemTags.collection.get('123').toJSON() + ]); + }); + it('fetches tag list from the global collection', function() { + var callback = sinon.stub(); + var opts = select2Stub.firstCall.args[0]; + + $tagsField.val('123'); + + opts.query({ + term: 'de', + callback: callback + }); + + expect(fetchTagsStub.calledOnce).toEqual(true); + expect(callback.notCalled).toEqual(true); + fetchTagsStub.yieldTo('success', fetchTagsStub.thisValues[0]); + + expect(callback.calledOnce).toEqual(true); + expect(callback.lastCall.args[0]).toEqual({ + results: [ + OC.SystemTags.collection.get('456').toJSON() + ] + }); + }); + it('reloads file list after selection', function() { + var reloadStub = sinon.stub(fileList, 'reload'); + $tagsField.val('456,123').change(); + expect(reloadStub.calledOnce).toEqual(true); + reloadStub.restore(); + }); + it('updates URL after selection', function() { + var handler = sinon.stub(); + fileList.$el.on('changeDirectory', handler); + $tagsField.val('456,123').change(); + + expect(handler.calledOnce).toEqual(true); + expect(handler.lastCall.args[0].dir).toEqual('456/123'); + }); + it('updates tag selection when url changed', function() { + fileList.$el.trigger(new $.Event('urlChanged', {dir: '456/123'})); + + expect(select2Stub.lastCall.args[0]).toEqual('val'); + expect(select2Stub.lastCall.args[1]).toEqual(['456', '123']); + }); + }); + + describe('loading results', function() { + var getFilteredFilesSpec, requestDeferred; + + beforeEach(function() { + requestDeferred = new $.Deferred(); + getFilteredFilesSpec = sinon.stub(OC.Files.Client.prototype, 'getFilteredFiles') + .returns(requestDeferred.promise()); + }); + afterEach(function() { + getFilteredFilesSpec.restore(); + }); + + it('renders empty message when no tags were set', function() { + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: [] + } + ); + + fileList.reload(); + + expect(fileList.$el.find('#emptycontent').hasClass('hidden')).toEqual(false); + + expect(getFilteredFilesSpec.notCalled).toEqual(true); + }); + + it('render files', function() { + fileList = new OCA.SystemTags.FileList( + $('#app-content-container'), { + systemTagIds: ['123', '456'] + } + ); + + fileList.reload(); + + expect(getFilteredFilesSpec.calledOnce).toEqual(true); + expect(getFilteredFilesSpec.lastCall.args[0].systemTagIds).toEqual(['123', '456']); + + var testFiles = [new FileInfo({ + id: 1, + type: 'file', + name: 'One.txt', + mimetype: 'text/plain', + mtime: 123456789, + size: 12, + etag: 'abc', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 2, + type: 'file', + name: 'Two.jpg', + mimetype: 'image/jpeg', + mtime: 234567890, + size: 12049, + etag: 'def', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 3, + type: 'file', + name: 'Three.pdf', + mimetype: 'application/pdf', + mtime: 234560000, + size: 58009, + etag: '123', + permissions: OC.PERMISSION_ALL + }), new FileInfo({ + id: 4, + type: 'dir', + name: 'somedir', + mimetype: 'httpd/unix-directory', + mtime: 134560000, + size: 250, + etag: '456', + permissions: OC.PERMISSION_ALL + })]; + + requestDeferred.resolve(207, testFiles); + + expect(fileList.$el.find('#emptycontent').hasClass('hidden')).toEqual(true); + expect(fileList.$el.find('tbody>tr').length).toEqual(4); + }); + }); +}); diff --git a/core/js/files/client.js b/core/js/files/client.js index b736447d65e..55a8e2c485a 100644 --- a/core/js/files/client.js +++ b/core/js/files/client.js @@ -241,7 +241,7 @@ path = decodeURIComponent(path); - if (response.propStat.length === 1 && response.propStat[0].status !== 200) { + if (response.propStat.length === 0 || response.propStat[0].status !== 'HTTP/1.1 200 OK') { return null; } @@ -415,6 +415,77 @@ }, /** + * Fetches a flat list of files filtered by a given filter criteria. + * (currently only system tags is supported) + * + * @param {Object} filter filter criteria + * @param {Object} [filter.systemTagIds] list of system tag ids to filter by + * @param {Object} [options] options + * @param {Array} [options.properties] list of Webdav properties to retrieve + * + * @return {Promise} promise + */ + getFilteredFiles: function(filter, options) { + options = options || {}; + var self = this; + var deferred = $.Deferred(); + var promise = deferred.promise(); + var properties; + if (_.isUndefined(options.properties)) { + properties = this.getPropfindProperties(); + } else { + properties = options.properties; + } + + if (!filter || !filter.systemTagIds || !filter.systemTagIds.length) { + throw 'Missing filter argument'; + } + + var headers = _.extend({}, this._defaultHeaders); + // root element with namespaces + var body = '<oc:filter-files '; + var namespace; + for (namespace in this._client.xmlNamespaces) { + body += ' xmlns:' + this._client.xmlNamespaces[namespace] + '="' + namespace + '"'; + } + body += '>\n'; + + // properties query + body += ' <' + this._client.xmlNamespaces['DAV:'] + ':prop>\n'; + _.each(properties, function(prop) { + var property = self._client.parseClarkNotation(prop); + body += ' <' + self._client.xmlNamespaces[property.namespace] + ':' + property.name + ' />\n'; + }); + + body += ' </' + this._client.xmlNamespaces['DAV:'] + ':prop>\n'; + + // rules block + body += ' <oc:filter-rules>\n'; + _.each(filter.systemTagIds, function(systemTagIds) { + body += ' <oc:systemtag>' + escapeHTML(systemTagIds) + '</oc:systemtag>\n'; + }); + body += ' </oc:filter-rules>\n'; + + // end of root + body += '</oc:filter-files>\n'; + + this._client.request( + 'REPORT', + this._buildUrl(), + headers, + body + ).then(function(result) { + if (self._isSuccessStatus(result.status)) { + var results = self._parseResult(result.body); + deferred.resolve(result.status, results); + } else { + deferred.reject(result.status); + } + }); + return promise; + }, + + /** * Returns the file info of a given path. * * @param {String} path path diff --git a/core/js/systemtags/systemtagsinputfield.js b/core/js/systemtags/systemtagsinputfield.js index 48fc98c6188..a64e5386102 100644 --- a/core/js/systemtags/systemtagsinputfield.js +++ b/core/js/systemtags/systemtagsinputfield.js @@ -425,6 +425,10 @@ } }, + getValues: function() { + this.$tagsField.select2('val'); + }, + setValues: function(values) { this.$tagsField.select2('val', values); }, diff --git a/core/js/tests/specs/files/clientSpec.js b/core/js/tests/specs/files/clientSpec.js index b945e1bb4da..7673ec6e0fc 100644 --- a/core/js/tests/specs/files/clientSpec.js +++ b/core/js/tests/specs/files/clientSpec.js @@ -318,6 +318,160 @@ describe('OC.Files.Client tests', function() { }); }); + describe('file filtering', function() { + + // TODO: switch this to the already parsed structure + var folderContentsXml = dav.Client.prototype.parseMultiStatus( + '<?xml version="1.0" encoding="utf-8"?>' + + '<d:multistatus xmlns:d="DAV:" xmlns:s="http://sabredav.org/ns" xmlns:oc="http://owncloud.org/ns">' + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 10:00:05 GMT', + 'd:getetag': '"56cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000011oc2d13a6a068', + 'oc:fileid': '11', + 'oc:permissions': 'RDNVCK', + 'oc:size': '120' + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/One.txt', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 13:38:05 GMT', + 'd:getetag': '"559fcabd79a38"', + 'd:getcontenttype': 'text/plain', + 'd:getcontentlength': 250, + 'd:resourcetype': '', + 'oc:id': '00000051oc2d13a6a068', + 'oc:fileid': '51', + 'oc:permissions': 'RDNVW' + }, + [ + 'oc:size', + ] + ) + + makeResponseBlock( + '/owncloud/remote.php/webdav/path/to%20space/%E6%96%87%E4%BB%B6%E5%A4%B9/sub', + { + 'd:getlastmodified': 'Fri, 10 Jul 2015 14:00:00 GMT', + 'd:getetag': '"66cfcabd79abb"', + 'd:resourcetype': '<d:collection/>', + 'oc:id': '00000015oc2d13a6a068', + 'oc:fileid': '15', + 'oc:permissions': 'RDNVCK', + 'oc:size': '100' + }, + [ + 'd:getcontenttype', + 'd:getcontentlength' + ] + ) + + '</d:multistatus>' + ); + + it('sends REPORT with filter information', function() { + client.getFilteredFiles({ + systemTagIds: ['123', '456'] + }); + + expect(requestStub.calledOnce).toEqual(true); + expect(requestStub.lastCall.args[0]).toEqual('REPORT'); + expect(requestStub.lastCall.args[1]).toEqual(baseUrl); + + var body = requestStub.lastCall.args[3]; + var doc = (new window.DOMParser()).parseFromString( + body, + 'application/xml' + ); + + var ns = 'http://owncloud.org/ns'; + expect(doc.documentElement.localName).toEqual('filter-files'); + expect(doc.documentElement.namespaceURI).toEqual(ns); + + var filterRoots = doc.getElementsByTagNameNS(ns, 'filter-rules'); + var rulesList = filterRoots[0] = doc.getElementsByTagNameNS(ns, 'systemtag'); + expect(rulesList.length).toEqual(2); + expect(rulesList[0].localName).toEqual('systemtag'); + expect(rulesList[0].namespaceURI).toEqual(ns); + expect(rulesList[0].textContent).toEqual('123'); + expect(rulesList[1].localName).toEqual('systemtag'); + expect(rulesList[1].namespaceURI).toEqual(ns); + expect(rulesList[1].textContent).toEqual('456'); + }); + it('sends REPORT with explicit properties to filter file list', function() { + client.getFilteredFiles({ + systemTagIds: ['123', '456'] + }); + + expect(requestStub.calledOnce).toEqual(true); + expect(requestStub.lastCall.args[0]).toEqual('REPORT'); + expect(requestStub.lastCall.args[1]).toEqual(baseUrl); + + var props = getRequestedProperties(requestStub.lastCall.args[3]); + expect(props).toContain('{DAV:}getlastmodified'); + expect(props).toContain('{DAV:}getcontentlength'); + expect(props).toContain('{DAV:}getcontenttype'); + expect(props).toContain('{DAV:}getetag'); + expect(props).toContain('{DAV:}resourcetype'); + expect(props).toContain('{http://owncloud.org/ns}fileid'); + expect(props).toContain('{http://owncloud.org/ns}size'); + expect(props).toContain('{http://owncloud.org/ns}permissions'); + }); + it('parses the result list into a FileInfo array', function() { + var promise = client.getFilteredFiles({ + systemTagIds: ['123', '456'] + }); + + expect(requestStub.calledOnce).toEqual(true); + + requestDeferred.resolve({ + status: 207, + body: folderContentsXml + }); + + promise.then(function(status, response) { + expect(status).toEqual(207); + expect(_.isArray(response)).toEqual(true); + + // returns all entries + expect(response.length).toEqual(3); + + // file entry + var info = response[0]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(11); + + // file entry + var info = response[1]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(51); + + // sub entry + info = response[2]; + expect(info instanceof OC.Files.FileInfo).toEqual(true); + expect(info.id).toEqual(15); + }); + }); + it('throws exception if arguments are missing', function() { + var thrown = null; + try { + client.getFilteredFiles({ + systemTagIds: [] + }); + } catch (e) { + thrown = true; + } + + expect(thrown).toEqual(true); + }); + }); + describe('file info', function() { var responseXml = dav.Client.prototype.parseMultiStatus( '<?xml version="1.0" encoding="utf-8"?>' + diff --git a/tests/karma.config.js b/tests/karma.config.js index 2b569fb7584..111af7a1559 100644 --- a/tests/karma.config.js +++ b/tests/karma.config.js @@ -101,6 +101,7 @@ module.exports = function(config) { // need to enforce loading order... 'apps/systemtags/js/app.js', 'apps/systemtags/js/systemtagsinfoview.js', + 'apps/systemtags/js/systemtagsfilelist.js', 'apps/systemtags/js/filesplugin.js' ], testFiles: ['apps/systemtags/tests/js/**/*.js'] |