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'); |
||||
} |
After Width: | Height: | Size: 293 B |
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