diff options
author | Vincent Petry <pvince81@owncloud.com> | 2016-02-08 11:43:42 +0100 |
---|---|---|
committer | Vincent Petry <pvince81@owncloud.com> | 2016-02-09 10:59:29 +0100 |
commit | e378a757fffa3e43a798c0bce7d2d831912bcf75 (patch) | |
tree | f5af92a98e4cc048b169f06fba1b7be37318fee7 /apps | |
parent | ae367c7e97b99885c7cb1feadae22aa1bb6cb729 (diff) | |
download | nextcloud-server-e378a757fffa3e43a798c0bce7d2d831912bcf75.tar.gz nextcloud-server-e378a757fffa3e43a798c0bce7d2d831912bcf75.zip |
Add system tags filter section for files app
Diffstat (limited to 'apps')
-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 |
10 files changed, 666 insertions, 1 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); + }); + }); +}); |