From e378a757fffa3e43a798c0bce7d2d831912bcf75 Mon Sep 17 00:00:00 2001
From: Vincent Petry <pvince81@owncloud.com>
Date: Mon, 8 Feb 2016 11:43:42 +0100
Subject: [PATCH] Add system tags filter section for files app

---
 apps/systemtags/appinfo/app.php               |  14 +
 apps/systemtags/css/systemtagsfilelist.css    |  29 +++
 apps/systemtags/img/tag.png                   | Bin 0 -> 293 bytes
 apps/systemtags/img/tag.svg                   |   5 +
 apps/systemtags/js/app.js                     |  87 +++++++
 apps/systemtags/js/filesplugin.js             |   3 +-
 apps/systemtags/js/systemtagsfilelist.js      | 240 ++++++++++++++++++
 apps/systemtags/list.php                      |  25 ++
 apps/systemtags/templates/list.php            |  38 +++
 .../tests/js/systemtagsfilelistSpec.js        | 226 +++++++++++++++++
 core/js/files/client.js                       |  73 +++++-
 core/js/systemtags/systemtagsinputfield.js    |   4 +
 core/js/tests/specs/files/clientSpec.js       | 154 +++++++++++
 tests/karma.config.js                         |   1 +
 14 files changed, 897 insertions(+), 2 deletions(-)
 create mode 100644 apps/systemtags/css/systemtagsfilelist.css
 create mode 100644 apps/systemtags/img/tag.png
 create mode 100644 apps/systemtags/img/tag.svg
 create mode 100644 apps/systemtags/js/systemtagsfilelist.js
 create mode 100644 apps/systemtags/list.php
 create mode 100644 apps/systemtags/templates/list.php
 create mode 100644 apps/systemtags/tests/js/systemtagsfilelistSpec.js

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
new file mode 100644
index 0000000000000000000000000000000000000000..5f4767a6f461956689bc15b231cafe91e5690481
GIT binary patch
literal 293
zcmeAS@N?(olHy`uVBq!ia0vp^0wB!63?wyl`GbK}NPtg>>;M1%flTUx4SfrRfhNk7
z1o;Ish)c=HDyV4a7}|Jv`bT8s<`ou~mRD3&*VZ>wM9eS(s!Q>7aSW-rmFs_9s6m0}
zG8dnSj0VTg|NleY$GCo;l)Y=x8~#HP*++s`cgP0b{VQQIk?-Ry!wc-0GlDlPXyz~z
zJyx>RyKm9Md`9)SV{O~!JyiWXTR*~k*#zem6>9zSmQM-(*t?IhO`lEmw`2NGkWW2b
L{an^LB{Ts5S9eS3

literal 0
HcmV?d00001

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;
 			}
 
@@ -414,6 +414,77 @@
 			return promise;
 		},
 
+		/**
+		 * 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.
 		 *
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']
-- 
GitLab