mirror of https://github.com/nextcloud/server.git
Add system tags filter section for files app
parent
ae367c7e97
commit
e378a757ff
@ -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');
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 293 B |
@ -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>
|
After Width: | Height: | Size: 813 B |
@ -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);
|
||||
});
|
||||
});
|
||||
});
|
Loading…
Reference in New Issue