@@ -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') | |||
) | |||
); |
@@ -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'); | |||
} |
@@ -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> |
@@ -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(); | |||
}); | |||
}); |
@@ -23,7 +23,8 @@ | |||
OCA.SystemTags.FilesPlugin = { | |||
allowedLists: [ | |||
'files', | |||
'favorites' | |||
'favorites', | |||
'systemtagsfilter' | |||
], | |||
attach: function(fileList) { |
@@ -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; | |||
})(); |
@@ -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(); |
@@ -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="" /> | |||
@@ -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); | |||
}); | |||
}); | |||
}); |
@@ -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. | |||
* |
@@ -425,6 +425,10 @@ | |||
} | |||
}, | |||
getValues: function() { | |||
this.$tagsField.select2('val'); | |||
}, | |||
setValues: function(values) { | |||
this.$tagsField.select2('val', values); | |||
}, |
@@ -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"?>' + |
@@ -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'] |