diff --git a/.gitignore b/.gitignore
index 3fb848dbb4..7d42be773b 100644
--- a/.gitignore
+++ b/.gitignore
@@ -21,7 +21,8 @@
!/apps/files_versions
!/apps/user_ldap
!/apps/user_webdavauth
-!apps/provisioning_api
+!/apps/provisioning_api
+!/apps/systemtags
/apps/files_external/3rdparty/irodsphp/PHPUnitTest
/apps/files_external/3rdparty/irodsphp/web
/apps/files_external/3rdparty/irodsphp/prods/test
diff --git a/apps/dav/lib/systemtag/systemtagplugin.php b/apps/dav/lib/systemtag/systemtagplugin.php
index 2cab9ba8d5..d81ccbf84b 100644
--- a/apps/dav/lib/systemtag/systemtagplugin.php
+++ b/apps/dav/lib/systemtag/systemtagplugin.php
@@ -127,7 +127,7 @@ class SystemTagPlugin extends \Sabre\DAV\ServerPlugin {
$url .= '/';
}
- $response->setHeader('Location', $url . $tag->getId());
+ $response->setHeader('Content-Location', $url . $tag->getId());
// created
$response->setStatus(201);
diff --git a/apps/dav/tests/unit/systemtag/systemtagplugin.php b/apps/dav/tests/unit/systemtag/systemtagplugin.php
index e0fbd40f5b..1d22af7518 100644
--- a/apps/dav/tests/unit/systemtag/systemtagplugin.php
+++ b/apps/dav/tests/unit/systemtag/systemtagplugin.php
@@ -201,7 +201,7 @@ class SystemTagPlugin extends \Test\TestCase {
$response->expects($this->once())
->method('setHeader')
- ->with('Location', 'http://example.com/dav/systemtags/1');
+ ->with('Content-Location', 'http://example.com/dav/systemtags/1');
$this->plugin->httpPost($request, $response);
}
@@ -266,7 +266,7 @@ class SystemTagPlugin extends \Test\TestCase {
$response->expects($this->once())
->method('setHeader')
- ->with('Location', 'http://example.com/dav/systemtags/1');
+ ->with('Content-Location', 'http://example.com/dav/systemtags/1');
$this->plugin->httpPost($request, $response);
}
diff --git a/apps/systemtags/appinfo/app.php b/apps/systemtags/appinfo/app.php
new file mode 100644
index 0000000000..d07902f777
--- /dev/null
+++ b/apps/systemtags/appinfo/app.php
@@ -0,0 +1,40 @@
+
+ *
+ * @copyright Copyright (c) 2015, 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
+ *
+ */
+
+$eventDispatcher = \OC::$server->getEventDispatcher();
+$eventDispatcher->addListener(
+ 'OCA\Files::loadAdditionalScripts',
+ function() {
+ // FIXME: no public API for these ?
+ \OC_Util::addVendorScript('select2/select2');
+ \OC_Util::addVendorStyle('select2/select2');
+ \OCP\Util::addScript('select2-toggleselect');
+ \OCP\Util::addScript('oc-backbone-webdav');
+ \OCP\Util::addScript('systemtags/systemtagmodel');
+ \OCP\Util::addScript('systemtags/systemtagsmappingcollection');
+ \OCP\Util::addScript('systemtags/systemtagscollection');
+ \OCP\Util::addScript('systemtags/systemtagsinputfield');
+ \OCP\Util::addScript('systemtags', 'app');
+ \OCP\Util::addScript('systemtags', 'filesplugin');
+ \OCP\Util::addScript('systemtags', 'systemtagsinfoview');
+ \OCP\Util::addStyle('systemtags');
+ }
+);
diff --git a/apps/systemtags/appinfo/info.xml b/apps/systemtags/appinfo/info.xml
new file mode 100644
index 0000000000..f2464f0413
--- /dev/null
+++ b/apps/systemtags/appinfo/info.xml
@@ -0,0 +1,18 @@
+
+
+ systemtags
+ System tags
+ System-wide tags user interface
+ AGPL
+ Vincent Petry
+ true
+
+
+ 0.1
+
+
+
+
+ user-systemtags
+
+
diff --git a/apps/systemtags/js/app.js b/apps/systemtags/js/app.js
new file mode 100644
index 0000000000..f55aa5c9a6
--- /dev/null
+++ b/apps/systemtags/js/app.js
@@ -0,0 +1,20 @@
+/*
+ * Copyright (c) 2015 Vincent Petry
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ if (!OCA.SystemTags) {
+ /**
+ * @namespace
+ */
+ OCA.SystemTags = {};
+ }
+
+})();
+
diff --git a/apps/systemtags/js/filesplugin.js b/apps/systemtags/js/filesplugin.js
new file mode 100644
index 0000000000..471440c2e0
--- /dev/null
+++ b/apps/systemtags/js/filesplugin.js
@@ -0,0 +1,41 @@
+/*
+ * Copyright (c) 2015 Vincent Petry
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function() {
+ OCA.SystemTags = _.extend({}, OCA.SystemTags);
+ if (!OCA.SystemTags) {
+ /**
+ * @namespace
+ */
+ OCA.SystemTags = {};
+ }
+
+ /**
+ * @namespace
+ */
+ OCA.SystemTags.FilesPlugin = {
+ allowedLists: [
+ 'files',
+ 'favorites'
+ ],
+
+ attach: function(fileList) {
+ if (this.allowedLists.indexOf(fileList.id) < 0) {
+ return;
+ }
+
+ fileList.registerDetailView(new OCA.SystemTags.SystemTagsInfoView());
+ }
+ };
+
+})();
+
+OC.Plugins.register('OCA.Files.FileList', OCA.SystemTags.FilesPlugin);
+
diff --git a/apps/systemtags/js/systemtagsinfoview.js b/apps/systemtags/js/systemtagsinfoview.js
new file mode 100644
index 0000000000..b1820bfcd9
--- /dev/null
+++ b/apps/systemtags/js/systemtagsinfoview.js
@@ -0,0 +1,135 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OCA) {
+ /**
+ * @class OCA.SystemTags.SystemTagsInfoView
+ * @classdesc
+ *
+ * Displays a file's system tags
+ *
+ */
+ var SystemTagsInfoView = OCA.Files.DetailFileInfoView.extend(
+ /** @lends OCA.SystemTags.SystemTagsInfoView.prototype */ {
+
+ _rendered: false,
+
+ className: 'systemTagsInfoView hidden',
+
+ /**
+ * @type OC.SystemTags.SystemTagsInputField
+ */
+ _inputView: null,
+
+ initialize: function(options) {
+ var self = this;
+ options = options || {};
+
+ this._inputView = new OC.SystemTags.SystemTagsInputField({
+ multiple: true,
+ allowActions: true,
+ allowCreate: true,
+ initSelection: function(element, callback) {
+ callback(self.selectedTagsCollection.toJSON());
+ }
+ });
+
+ this.selectedTagsCollection = new OC.SystemTags.SystemTagsMappingCollection([], {objectType: 'files'});
+
+ this._inputView.collection.on('change:name', this._onTagRenamedGlobally, this);
+ this._inputView.collection.on('remove', this._onTagDeletedGlobally, this);
+
+ this._inputView.on('select', this._onSelectTag, this);
+ this._inputView.on('deselect', this._onDeselectTag, this);
+ },
+
+ /**
+ * Event handler whenever a tag was selected
+ */
+ _onSelectTag: function(tag) {
+ // create a mapping entry for this tag
+ this.selectedTagsCollection.create(tag.toJSON());
+ },
+
+ /**
+ * Event handler whenever a tag gets deselected.
+ * Removes the selected tag from the mapping collection.
+ *
+ * @param {string} tagId tag id
+ */
+ _onDeselectTag: function(tagId) {
+ this.selectedTagsCollection.get(tagId).destroy();
+ },
+
+ /**
+ * Event handler whenever a tag was renamed globally.
+ *
+ * This will automatically adjust the tag mapping collection to
+ * container the new name.
+ *
+ * @param {OC.Backbone.Model} changedTag tag model that has changed
+ */
+ _onTagRenamedGlobally: function(changedTag) {
+ // also rename it in the selection, if applicable
+ var selectedTagMapping = this.selectedTagsCollection.get(changedTag.id);
+ if (selectedTagMapping) {
+ selectedTagMapping.set(changedTag.toJSON());
+ }
+ },
+
+ /**
+ * Event handler whenever a tag was deleted globally.
+ *
+ * This will automatically adjust the tag mapping collection to
+ * container the new name.
+ *
+ * @param {OC.Backbone.Model} changedTag tag model that has changed
+ */
+ _onTagDeletedGlobally: function(tagId) {
+ // also rename it in the selection, if applicable
+ this.selectedTagsCollection.remove(tagId);
+ },
+
+ setFileInfo: function(fileInfo) {
+ var self = this;
+ if (!this._rendered) {
+ this.render();
+ }
+
+ if (fileInfo) {
+ this.selectedTagsCollection.setObjectId(fileInfo.id);
+ this.selectedTagsCollection.fetch({
+ success: function(collection) {
+ collection.fetched = true;
+ self._inputView.setData(collection.toJSON());
+ self.$el.removeClass('hidden');
+ }
+ });
+ }
+ this.$el.addClass('hidden');
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ this.$el.append(this._inputView.$el);
+ this._inputView.render();
+ },
+
+ remove: function() {
+ this._inputView.remove();
+ }
+ });
+
+ OCA.SystemTags.SystemTagsInfoView = SystemTagsInfoView;
+
+})(OCA);
+
diff --git a/apps/systemtags/tests/js/systemtagsinfoviewSpec.js b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js
new file mode 100644
index 0000000000..971ad8fc17
--- /dev/null
+++ b/apps/systemtags/tests/js/systemtagsinfoviewSpec.js
@@ -0,0 +1,149 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2016 Vincent Petry
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 along with this library. If not, see .
+*
+*/
+
+describe('OCA.SystemTags.SystemTagsInfoView tests', function() {
+ var view;
+
+ beforeEach(function() {
+ view = new OCA.SystemTags.SystemTagsInfoView();
+ $('#testArea').append(view.$el);
+ });
+ afterEach(function() {
+ view.remove();
+ view = undefined;
+ });
+ describe('rendering', function() {
+ it('renders input field view', function() {
+ view.render();
+ expect(view.$el.find('input[name=tags]').length).toEqual(1);
+ });
+ it('fetches selected tags then renders when setting file info', function() {
+ var fetchStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'fetch');
+ var setDataStub = sinon.stub(OC.SystemTags.SystemTagsInputField.prototype, 'setData');
+
+ expect(view.$el.hasClass('hidden')).toEqual(true);
+
+ view.setFileInfo({id: '123'});
+ expect(view.$el.find('input[name=tags]').length).toEqual(1);
+
+ expect(fetchStub.calledOnce).toEqual(true);
+ expect(view.selectedTagsCollection.url())
+ .toEqual(OC.linkToRemote('dav') + '/systemtags-relations/files/123');
+
+ view.selectedTagsCollection.add([
+ {id: '1', name: 'test1'},
+ {id: '3', name: 'test3'}
+ ]);
+
+ fetchStub.yieldTo('success', view.selectedTagsCollection);
+ expect(setDataStub.calledOnce).toEqual(true);
+ expect(setDataStub.getCall(0).args[0]).toEqual([{
+ id: '1', name: 'test1', userVisible: true, userAssignable: true
+ }, {
+ id: '3', name: 'test3', userVisible: true, userAssignable: true
+ }]);
+
+ expect(view.$el.hasClass('hidden')).toEqual(false);
+
+ fetchStub.restore();
+ setDataStub.restore();
+ });
+ it('overrides initSelection to use the local collection', function() {
+ var inputViewSpy = sinon.spy(OC.SystemTags, 'SystemTagsInputField');
+ var element = $('');
+ view.remove();
+ view = new OCA.SystemTags.SystemTagsInfoView();
+ view.selectedTagsCollection.add([
+ {id: '1', name: 'test1'},
+ {id: '3', name: 'test3'}
+ ]);
+
+ var callback = sinon.stub();
+ inputViewSpy.getCall(0).args[0].initSelection(element, callback);
+
+ expect(callback.calledOnce).toEqual(true);
+ expect(callback.getCall(0).args[0]).toEqual([{
+ id: '1', name: 'test1', userVisible: true, userAssignable: true
+ }, {
+ id: '3', name: 'test3', userVisible: true, userAssignable: true
+ }]);
+
+ inputViewSpy.restore();
+ });
+ });
+ describe('events', function() {
+ var allTagsCollection;
+ beforeEach(function() {
+ allTagsCollection = view._inputView.collection;
+
+ allTagsCollection.add([
+ {id: '1', name: 'test1'},
+ {id: '2', name: 'test2'},
+ {id: '3', name: 'test3'}
+ ]);
+
+ view.selectedTagsCollection.add([
+ {id: '1', name: 'test1'},
+ {id: '3', name: 'test3'}
+ ]);
+ view.render();
+ });
+
+ it('renames model in selection collection on rename', function() {
+ allTagsCollection.get('3').set('name', 'test3_renamed');
+
+ expect(view.selectedTagsCollection.get('3').get('name')).toEqual('test3_renamed');
+ });
+
+ it('adds tag to selection collection when selected by input', function() {
+ var createStub = sinon.stub(OC.SystemTags.SystemTagsMappingCollection.prototype, 'create');
+ view._inputView.trigger('select', allTagsCollection.get('2'));
+
+ expect(createStub.calledOnce).toEqual(true);
+ expect(createStub.getCall(0).args[0]).toEqual({
+ id: '2',
+ name: 'test2',
+ userVisible: true,
+ userAssignable: true
+ });
+
+ createStub.restore();
+ });
+ it('removes tag from selection collection when deselected by input', function() {
+ var destroyStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'destroy');
+ view._inputView.trigger('deselect', '3');
+
+ expect(destroyStub.calledOnce).toEqual(true);
+ expect(destroyStub.calledOn(view.selectedTagsCollection.get('3'))).toEqual(true);
+
+ destroyStub.restore();
+ });
+
+ it('removes tag from selection whenever the tag was deleted globally', function() {
+ expect(view.selectedTagsCollection.get('3')).not.toBeFalsy();
+
+ allTagsCollection.remove('3');
+
+ expect(view.selectedTagsCollection.get('3')).toBeFalsy();
+
+ });
+ });
+});
diff --git a/core/css/systemtags.css b/core/css/systemtags.css
new file mode 100644
index 0000000000..5c667e5454
--- /dev/null
+++ b/core/css/systemtags.css
@@ -0,0 +1,80 @@
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+.systemtags-select2-dropdown .select2-selected {
+ display: list-item;
+ background-color: #f8f8f8;
+}
+.systemtags-select2-dropdown .select2-highlighted,
+.systemtags-select2-dropdown .select2-selected.select2-highlighted {
+ background: #3875d7;
+}
+
+.systemtags-select2-dropdown .select2-highlighted {
+ color: #000000;
+}
+.systemtags-select2-dropdown .select2-result-label .checkmark {
+ visibility: hidden;
+}
+
+.systemtags-select2-dropdown .select2-result-label .new-item .systemtags-actions {
+ display: none;
+}
+
+.systemtags-select2-dropdown .select2-selected .select2-result-label .checkmark {
+ visibility: visible;
+}
+
+.systemtags-select2-dropdown .select2-result-label .icon {
+ display: inline-block;
+}
+
+.systemtags-select2-dropdown .systemtags-actions {
+ float: right;
+}
+
+.systemtags-select2-dropdown .systemtags-rename-form {
+ display: inline;
+ margin-left: 10px;
+}
+
+.systemtags-select2-container {
+ width: 80%;
+}
+
+.systemtags-select2-container .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: 7px 6px 5px;
+ min-height: auto;
+}
+
+.systemtags-select2-container .select2-choices .select2-search-choice {
+ border: 0;
+ box-shadow: none;
+ background: none;
+ padding: 0;
+ margin: 0;
+ line-height: 20px;
+}
+.systemtags-select2-container .select2-choices .select2-search-choice-close {
+ display: none;
+}
+.systemtags-select2-container .select2-choices .select2-search-field input {
+ margin: 0;
+ padding: 0;
+ line-height: 20px;
+}
+
diff --git a/core/js/core.json b/core/js/core.json
index 43cb1b472f..93c64afea9 100644
--- a/core/js/core.json
+++ b/core/js/core.json
@@ -44,6 +44,10 @@
"mimetype.js",
"mimetypelist.js",
"files/fileinfo.js",
- "files/client.js"
+ "files/client.js",
+ "systemtags/systemtagmodel.js",
+ "systemtags/systemtagscollection.js",
+ "systemtags/systemtagsmappingcollection.js",
+ "systemtags/systemtagsinputfield.js"
]
}
diff --git a/core/js/oc-backbone-webdav.js b/core/js/oc-backbone-webdav.js
index 3ca31902c9..24a2bb5019 100644
--- a/core/js/oc-backbone-webdav.js
+++ b/core/js/oc-backbone-webdav.js
@@ -167,7 +167,7 @@
}
function callPropPatch(client, options, model, headers) {
- client.propPatch(
+ return client.propPatch(
options.url,
convertModelAttributesToDavProperties(model.changed, options.davProperties),
headers
diff --git a/core/js/select2-toggleselect.js b/core/js/select2-toggleselect.js
index d38d4cc60b..4fada592a3 100644
--- a/core/js/select2-toggleselect.js
+++ b/core/js/select2-toggleselect.js
@@ -12,8 +12,6 @@
/**
* Select2 extension for toggling values in a multi-select dropdown
- *
- * Inspired by http://stackoverflow.com/a/27466159 and adjusted
*/
(function(Select2) {
@@ -28,28 +26,25 @@
var Select2TriggerSelect = Select2.class.multi.prototype.triggerSelect;
Select2.class.multi.prototype.triggerSelect = function (data) {
if (this.opts.toggleSelect && this.val().indexOf(this.id(data)) !== -1) {
-
- var val = this.id(data);
- var evt = $.Event('select2-removing');
- evt.val = val;
- evt.choice = data;
- this.opts.element.trigger(evt);
-
- if (evt.isDefaultPrevented()) {
- return false;
- }
-
var self = this;
- this.results.find('.select2-result.select2-selected').each(function () {
- var $this = $(this);
- if (self.id($this.data('select2-data')) === val) {
- $this.removeClass('select2-selected');
- }
+ var val = this.id(data);
+
+ var selectionEls = this.container.find('.select2-search-choice').filter(function() {
+ return (self.id($(this).data('select2-data')) === val);
});
- this.opts.element.trigger({ type: "select2-removed", val: this.id(data), choice: data });
- this.triggerChange({ removed: data });
+ if (this.unselect(selectionEls)) {
+ // also unselect in dropdown
+ this.results.find('.select2-result.select2-selected').each(function () {
+ var $this = $(this);
+ if (self.id($this.data('select2-data')) === val) {
+ $this.removeClass('select2-selected');
+ }
+ });
+ this.clearSearch();
+ }
+ return false;
} else {
return Select2TriggerSelect.apply(this, arguments);
}
diff --git a/core/js/systemtags/systemtagmodel.js b/core/js/systemtags/systemtagmodel.js
new file mode 100644
index 0000000000..ad6ea7b7d4
--- /dev/null
+++ b/core/js/systemtags/systemtagmodel.js
@@ -0,0 +1,52 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+ var NS_OWNCLOUD = 'http://owncloud.org/ns';
+ /**
+ * @class OCA.SystemTags.SystemTagsCollection
+ * @classdesc
+ *
+ * System tag
+ *
+ */
+ var SystemTagModel = OC.Backbone.Model.extend(
+ /** @lends OCA.SystemTags.SystemTagModel.prototype */ {
+ sync: OC.Backbone.davSync,
+
+ defaults: {
+ userVisible: true,
+ userAssignable: true
+ },
+
+ davProperties: {
+ 'id': '{' + NS_OWNCLOUD + '}id',
+ 'name': '{' + NS_OWNCLOUD + '}display-name',
+ 'userVisible': '{' + NS_OWNCLOUD + '}user-visible',
+ 'userAssignable': '{' + NS_OWNCLOUD + '}user-assignable'
+ },
+
+ parse: function(data) {
+ return {
+ id: data.id,
+ name: data.name,
+ userVisible: data.userVisible === '1',
+ userAssignable: data.userAssignable === '1'
+ };
+ }
+ });
+
+ /**
+ * @namespace
+ */
+ OC.SystemTags = OC.SystemTags || {};
+ OC.SystemTags.SystemTagModel = SystemTagModel;
+})(OC);
+
diff --git a/core/js/systemtags/systemtagscollection.js b/core/js/systemtags/systemtagscollection.js
new file mode 100644
index 0000000000..0f8a7aa980
--- /dev/null
+++ b/core/js/systemtags/systemtagscollection.js
@@ -0,0 +1,89 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+
+ function filterFunction(model, term) {
+ return model.get('name').substr(0, term.length) === term;
+ }
+
+ /**
+ * @class OCA.SystemTags.SystemTagsCollection
+ * @classdesc
+ *
+ * Collection of tags assigned to a file
+ *
+ */
+ var SystemTagsCollection = OC.Backbone.Collection.extend(
+ /** @lends OC.SystemTags.SystemTagsCollection.prototype */ {
+
+ sync: OC.Backbone.davSync,
+
+ model: OC.SystemTags.SystemTagModel,
+
+ url: function() {
+ return OC.linkToRemote('dav') + '/systemtags/';
+ },
+
+ filterByName: function(name) {
+ return this.filter(function(model) {
+ return filterFunction(model, name);
+ });
+ },
+
+ reset: function() {
+ this.fetched = false;
+ return OC.Backbone.Collection.prototype.reset.apply(this, arguments);
+ },
+
+ /**
+ * Lazy fetch.
+ * Only fetches once, subsequent calls will directly call the success handler.
+ *
+ * @param options
+ * @param [options.force] true to force fetch even if cached entries exist
+ *
+ * @see Backbone.Collection#fetch
+ */
+ fetch: function(options) {
+ var self = this;
+ options = options || {};
+ if (this.fetched || options.force) {
+ // directly call handler
+ if (options.success) {
+ options.success(this, null, options);
+ }
+ // trigger sync event
+ this.trigger('sync', this, null, options);
+ return Promise.resolve();
+ }
+
+ var success = options.success;
+ options = _.extend({}, options);
+ options.success = function() {
+ self.fetched = true;
+ if (success) {
+ return success.apply(this, arguments);
+ }
+ };
+
+ return OC.Backbone.Collection.prototype.fetch.call(this, options);
+ }
+ });
+
+ OC.SystemTags = OC.SystemTags || {};
+ OC.SystemTags.SystemTagsCollection = SystemTagsCollection;
+
+ /**
+ * @type OC.SystemTags.SystemTagsCollection
+ */
+ OC.SystemTags.collection = new OC.SystemTags.SystemTagsCollection();
+})(OC);
+
diff --git a/core/js/systemtags/systemtagsinputfield.js b/core/js/systemtags/systemtagsinputfield.js
new file mode 100644
index 0000000000..facacc50e2
--- /dev/null
+++ b/core/js/systemtags/systemtagsinputfield.js
@@ -0,0 +1,372 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+/* global Handlebars */
+
+(function(OC) {
+ var TEMPLATE =
+ '';
+
+ var RESULT_TEMPLATE =
+ '' +
+ ' ' +
+ ' {{name}}' +
+ '{{#allowActions}}' +
+ ' ' +
+ ' ' +
+ ' ' +
+ '{{/allowActions}}' +
+ '';
+
+ var RENAME_FORM_TEMPLATE =
+ '';
+
+ /**
+ * @class OC.SystemTags.SystemTagsInputField
+ * @classdesc
+ *
+ * Displays a file's system tags
+ *
+ */
+ var SystemTagsInputField = OC.Backbone.View.extend(
+ /** @lends OC.SystemTags.SystemTagsInputField.prototype */ {
+
+ _rendered: false,
+
+ _newTag: null,
+
+ className: 'systemTagsInputFieldContainer',
+
+ template: function(data) {
+ if (!this._template) {
+ this._template = Handlebars.compile(TEMPLATE);
+ }
+ return this._template(data);
+ },
+
+ /**
+ * Creates a new SystemTagsInputField
+ *
+ * @param {Object} [options]
+ * @param {string} [options.objectType=files] object type for which tags are assigned to
+ * @param {bool} [options.multiple=false] whether to allow selecting multiple tags
+ * @param {bool} [options.allowActions=true] whether tags can be renamed/delete within the dropdown
+ * @param {bool} [options.allowCreate=true] whether new tags can be created
+ * @param {Function} options.initSelection function to convert selection to data
+ */
+ initialize: function(options) {
+ options = options || {};
+
+ this._multiple = !!options.multiple;
+ this._allowActions = _.isUndefined(options.allowActions) || !!options.allowActions;
+ this._allowCreate = _.isUndefined(options.allowCreate) || !!options.allowCreate;
+
+ if (_.isFunction(options.initSelection)) {
+ this._initSelection = options.initSelection;
+ }
+
+ this.collection = options.collection || OC.SystemTags.collection;
+
+ var self = this;
+ this.collection.on('change:name remove', function() {
+ // refresh selection
+ _.defer(self._refreshSelection);
+ });
+
+ _.bindAll(
+ this,
+ '_refreshSelection',
+ '_onClickRenameTag',
+ '_onClickDeleteTag',
+ '_onSelectTag',
+ '_onDeselectTag',
+ '_onSubmitRenameTag'
+ );
+ },
+
+ /**
+ * Refreshes the selection, triggering a call to
+ * select2's initSelection
+ */
+ _refreshSelection: function() {
+ this.$tagsField.select2('val', this.$tagsField.val());
+ },
+
+ /**
+ * Event handler whenever the user clicked the "rename" action.
+ * This will display the rename field.
+ */
+ _onClickRenameTag: function(ev) {
+ var $item = $(ev.target).closest('.systemtags-item');
+ var tagId = $item.attr('data-id');
+ var tagModel = this.collection.get(tagId);
+ if (!this._renameFormTemplate) {
+ this._renameFormTemplate = Handlebars.compile(RENAME_FORM_TEMPLATE);
+ }
+
+ var oldName = tagModel.get('name');
+ var $renameForm = $(this._renameFormTemplate({
+ cid: this.cid,
+ name: oldName,
+ deleteTooltip: t('core', 'Delete'),
+ renameLabel: t('core', 'Rename'),
+ }));
+ $item.find('.label').after($renameForm);
+ $item.find('.label, .systemtags-actions').addClass('hidden');
+ $item.closest('.select2-result').addClass('has-form');
+
+ $renameForm.find('[title]').tooltip({
+ placement: 'bottom',
+ container: 'body'
+ });
+ $renameForm.find('input').focus().selectRange(0, oldName.length);
+ return false;
+ },
+
+ /**
+ * Event handler whenever the rename form has been submitted after
+ * the user entered a new tag name.
+ * This will submit the change to the server.
+ *
+ * @param {Object} ev event
+ */
+ _onSubmitRenameTag: function(ev) {
+ ev.preventDefault();
+ var $form = $(ev.target);
+ var $item = $form.closest('.systemtags-item');
+ var tagId = $item.attr('data-id');
+ var tagModel = this.collection.get(tagId);
+ var newName = $(ev.target).find('input').val();
+ if (newName && newName !== tagModel.get('name')) {
+ tagModel.save({'name': newName});
+ // TODO: spinner, and only change text after finished saving
+ $item.find('.label').text(newName);
+ }
+ $item.find('.label, .systemtags-actions').removeClass('hidden');
+ $form.remove();
+ $item.closest('.select2-result').removeClass('has-form');
+ },
+
+ /**
+ * Event handler whenever a tag must be deleted
+ *
+ * @param {Object} ev event
+ */
+ _onClickDeleteTag: function(ev) {
+ var $item = $(ev.target).closest('.systemtags-item');
+ var tagId = $item.attr('data-id');
+ this.collection.get(tagId).destroy();
+ $item.closest('.select2-result').remove();
+ // TODO: spinner
+ return false;
+ },
+
+ /**
+ * Event handler whenever a tag is selected.
+ * Also called whenever tag creation is requested through the dummy tag object.
+ *
+ * @param {Object} e event
+ */
+ _onSelectTag: function(e) {
+ var self = this;
+ var tag;
+ if (e.object && e.object.isNew) {
+ // newly created tag, check if existing
+ // create a new tag
+ tag = this.collection.create({
+ name: e.object.name,
+ userVisible: true,
+ userAssignable: true
+ }, {
+ success: function(model) {
+ var data = self.$tagsField.select2('data');
+ data.push(model.toJSON());
+ self.$tagsField.select2('data', data);
+ self.trigger('select', model);
+ }
+ });
+ this.$tagsField.select2('close');
+ e.preventDefault();
+ return false;
+ } else {
+ tag = this.collection.get(e.object.id);
+ }
+ this._newTag = null;
+ this.trigger('select', tag);
+ },
+
+ /**
+ * Event handler whenever a tag gets deselected.
+ *
+ * @param {Object} e event
+ */
+ _onDeselectTag: function(e) {
+ this.trigger('deselect', e.choice.id);
+ },
+
+ /**
+ * Autocomplete function for dropdown results
+ *
+ * @param {Object} query select2 query object
+ */
+ _queryTagsAutocomplete: function(query) {
+ var self = this;
+ this.collection.fetch({
+ success: function() {
+ query.callback({
+ results: _.invoke(self.collection.filterByName(query.term), 'toJSON')
+ });
+ }
+ });
+ },
+
+ _preventDefault: function(e) {
+ e.stopPropagation();
+ },
+
+ /**
+ * Formats a single dropdown result
+ *
+ * @param {Object} data data to format
+ * @return {string} HTML markup
+ */
+ _formatDropDownResult: function(data) {
+ if (!this._resultTemplate) {
+ this._resultTemplate = Handlebars.compile(RESULT_TEMPLATE);
+ }
+ return this._resultTemplate(_.extend({
+ renameTooltip: t('core', 'Rename'),
+ allowActions: this._allowActions
+ }, data));
+ },
+
+ /**
+ * Create new dummy choice for select2 when the user
+ * types an arbitrary string
+ *
+ * @param {string} term entered term
+ * @return {Object} dummy tag
+ */
+ _createSearchChoice: function(term) {
+ if (this.collection.filterByName(term).length) {
+ return;
+ }
+ if (!this._newTag) {
+ this._newTag = {
+ id: -1,
+ name: term,
+ isNew: true
+ };
+ } else {
+ this._newTag.name = term;
+ }
+
+ return this._newTag;
+ },
+
+ _initSelection: function(element, callback) {
+ var self = this;
+ var ids = $(element).val().split(',');
+
+ function findSelectedObjects(ids) {
+ var selectedModels = self.collection.filter(function(model) {
+ return ids.indexOf(model.id) >= 0;
+ });
+ return _.invoke(selectedModels, 'toJSON');
+ }
+
+ this.collection.fetch({
+ success: function() {
+ callback(findSelectedObjects(ids));
+ }
+ });
+ },
+
+ /**
+ * Renders this details view
+ */
+ render: function() {
+ var self = this;
+ this.$el.html(this.template());
+
+ this.$el.find('[title]').tooltip({placement: 'bottom'});
+ this.$tagsField = this.$el.find('[name=tags]');
+ this.$tagsField.select2({
+ placeholder: t('core', 'Global tags'),
+ containerCssClass: 'systemtags-select2-container',
+ dropdownCssClass: 'systemtags-select2-dropdown',
+ closeOnSelect: false,
+ allowClear: false,
+ multiple: this._multiple,
+ toggleSelect: this._multiple,
+ query: _.bind(this._queryTagsAutocomplete, this),
+ id: function(tag) {
+ return tag.id;
+ },
+ initSelection: _.bind(this._initSelection, this),
+ formatResult: _.bind(this._formatDropDownResult, this),
+ formatSelection: function(tag) {
+ return '' + escapeHTML(tag.name) + '' +
+ ', ';
+ },
+ createSearchChoice: this._allowCreate ? _.bind(this._createSearchChoice, this) : undefined,
+ sortResults: function(results) {
+ var selectedItems = _.pluck(self.$tagsField.select2('data'), 'id');
+ results.sort(function(a, b) {
+ var aSelected = selectedItems.indexOf(a.id) >= 0;
+ var bSelected = selectedItems.indexOf(b.id) >= 0;
+ if (aSelected === bSelected) {
+ return OC.Util.naturalSortCompare(a.name, b.name);
+ }
+ if (aSelected && !bSelected) {
+ return -1;
+ }
+ return 1;
+ });
+ return results;
+ }
+ })
+ .on('select2-selecting', this._onSelectTag)
+ .on('select2-removing', this._onDeselectTag);
+
+ var $dropDown = this.$tagsField.select2('dropdown');
+ // register events for inside the dropdown
+ $dropDown.on('mouseup', '.rename', this._onClickRenameTag);
+ $dropDown.on('mouseup', '.delete', this._onClickDeleteTag);
+ $dropDown.on('mouseup', '.select2-result-selectable.has-form', this._preventDefault);
+ $dropDown.on('submit', '.systemtags-rename-form', this._onSubmitRenameTag);
+
+ this.delegateEvents();
+ },
+
+ remove: function() {
+ if (this.$tagsField) {
+ this.$tagsField.select2('destroy');
+ }
+ },
+
+ setValues: function(values) {
+ this.$tagsField.select2('val', values);
+ },
+
+ setData: function(data) {
+ this.$tagsField.select2('data', data);
+ }
+ });
+
+ OC.SystemTags = OC.SystemTags || {};
+ OC.SystemTags.SystemTagsInputField = SystemTagsInputField;
+
+})(OC);
+
diff --git a/core/js/systemtags/systemtagsmappingcollection.js b/core/js/systemtags/systemtagsmappingcollection.js
new file mode 100644
index 0000000000..f30a9dbd98
--- /dev/null
+++ b/core/js/systemtags/systemtagsmappingcollection.js
@@ -0,0 +1,87 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+ /**
+ * @class OC.SystemTags.SystemTagsMappingCollection
+ * @classdesc
+ *
+ * Collection of tags assigned to a an object
+ *
+ */
+ var SystemTagsMappingCollection = OC.Backbone.Collection.extend(
+ /** @lends OC.SystemTags.SystemTagsMappingCollection.prototype */ {
+
+ sync: OC.Backbone.davSync,
+
+ /**
+ * Use PUT instead of PROPPATCH
+ */
+ usePUT: true,
+
+ /**
+ * Id of the file for which to filter activities by
+ *
+ * @var int
+ */
+ _objectId: null,
+
+ /**
+ * Type of the object to filter by
+ *
+ * @var string
+ */
+ _objectType: 'files',
+
+ model: OC.SystemTags.SystemTagModel,
+
+ url: function() {
+ return OC.linkToRemote('dav') + '/systemtags-relations/' + this._objectType + '/' + this._objectId;
+ },
+
+ /**
+ * Sets the object id to filter by or null for all.
+ *
+ * @param {int} objectId file id or null
+ */
+ setObjectId: function(objectId) {
+ this._objectId = objectId;
+ },
+
+ /**
+ * Sets the object type to filter by or null for all.
+ *
+ * @param {int} objectType file id or null
+ */
+ setObjectType: function(objectType) {
+ this._objectType = objectType;
+ },
+
+ initialize: function(models, options) {
+ options = options || {};
+ if (!_.isUndefined(options.objectId)) {
+ this._objectId = options.objectId;
+ }
+ if (!_.isUndefined(options.objectType)) {
+ this._objectType = options.objectType;
+ }
+ },
+
+ getTagIds: function() {
+ return this.map(function(model) {
+ return model.id;
+ });
+ }
+ });
+
+ OC.SystemTags = OC.SystemTags || {};
+ OC.SystemTags.SystemTagsMappingCollection = SystemTagsMappingCollection;
+})(OC);
+
diff --git a/core/js/tests/specHelper.js b/core/js/tests/specHelper.js
index f09a7054c9..d1c7873f0e 100644
--- a/core/js/tests/specHelper.js
+++ b/core/js/tests/specHelper.js
@@ -160,7 +160,7 @@ window.isPhantom = /phantom/i.test(navigator.userAgent);
OC.Plugins._plugins = [];
// dummy select2 (which isn't loaded during the tests)
- $.fn.select2 = function() {};
+ $.fn.select2 = function() { return this; };
});
afterEach(function() {
diff --git a/core/js/tests/specs/systemtags/systemtagscollectionSpec.js b/core/js/tests/specs/systemtags/systemtagscollectionSpec.js
new file mode 100644
index 0000000000..6f2d836175
--- /dev/null
+++ b/core/js/tests/specs/systemtags/systemtagscollectionSpec.js
@@ -0,0 +1,84 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2016 Vincent Petry
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 along with this library. If not, see .
+*
+*/
+
+describe('OC.SystemTags.SystemTagsCollection tests', function() {
+ var collection;
+
+ beforeEach(function() {
+ collection = new OC.SystemTags.SystemTagsCollection();
+ });
+ it('fetches only once, until reset', function() {
+ var syncStub = sinon.stub(collection, 'sync');
+ var callback = sinon.stub();
+ var callback2 = sinon.stub();
+ var callback3 = sinon.stub();
+ var eventHandler = sinon.stub();
+
+ collection.on('sync', eventHandler);
+
+ collection.fetch({
+ success: callback
+ });
+
+ expect(callback.notCalled).toEqual(true);
+ expect(syncStub.calledOnce).toEqual(true);
+ expect(eventHandler.notCalled).toEqual(true);
+
+ syncStub.yieldTo('success', collection);
+
+ expect(callback.calledOnce).toEqual(true);
+ expect(callback.firstCall.args[0]).toEqual(collection);
+ expect(eventHandler.calledOnce).toEqual(true);
+ expect(eventHandler.firstCall.args[0]).toEqual(collection);
+
+ collection.fetch({
+ success: callback2
+ });
+
+ expect(eventHandler.calledTwice).toEqual(true);
+ expect(eventHandler.secondCall.args[0]).toEqual(collection);
+
+ // not re-called
+ expect(syncStub.calledOnce).toEqual(true);
+
+ expect(callback.calledOnce).toEqual(true);
+ expect(callback2.calledOnce).toEqual(true);
+ expect(callback2.firstCall.args[0]).toEqual(collection);
+
+ expect(collection.fetched).toEqual(true);
+
+ collection.reset();
+
+ expect(collection.fetched).toEqual(false);
+
+ collection.fetch({
+ success: callback3
+ });
+
+ expect(syncStub.calledTwice).toEqual(true);
+
+ syncStub.yieldTo('success', collection);
+ expect(callback3.calledOnce).toEqual(true);
+ expect(callback3.firstCall.args[0]).toEqual(collection);
+
+ syncStub.restore();
+ });
+});
diff --git a/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js b/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js
new file mode 100644
index 0000000000..dc8d2ec82f
--- /dev/null
+++ b/core/js/tests/specs/systemtags/systemtagsinputfieldSpec.js
@@ -0,0 +1,308 @@
+/**
+* ownCloud
+*
+* @author Vincent Petry
+* @copyright 2016 Vincent Petry
+*
+* This library is free software; you can redistribute it and/or
+* modify it under the terms of the GNU AFFERO GENERAL PUBLIC LICENSE
+* License as published by the Free Software Foundation; either
+* version 3 of the License, or any later version.
+*
+* This library 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 along with this library. If not, see .
+*
+*/
+
+describe('OC.SystemTags.SystemTagsInputField tests', function() {
+ var view, select2Stub;
+
+ beforeEach(function() {
+ var $container = $('');
+ select2Stub = sinon.stub($.fn, 'select2');
+ select2Stub.returnsThis();
+ $('#testArea').append($container);
+ view = new OC.SystemTags.SystemTagsInputField();
+ $container.append(view.$el);
+ });
+ afterEach(function() {
+ select2Stub.restore();
+ OC.SystemTags.collection.reset();
+ view.remove();
+ view = undefined;
+ });
+ describe('rendering', function() {
+ beforeEach(function() {
+ view.render();
+ });
+ it('calls select2 on rendering', function() {
+ expect(view.$el.find('input[name=tags]').length).toEqual(1);
+ expect(select2Stub.called).toEqual(true);
+ });
+ it('formatResult renders rename button', function() {
+ var opts = select2Stub.getCall(0).args[0];
+ var $el = $(opts.formatResult({id: '1', name: 'test'}));
+ expect($el.find('.label').text()).toEqual('test');
+ expect($el.find('.rename').length).toEqual(1);
+ });
+ });
+ describe('initSelection', function() {
+ var fetchStub;
+ var testTags;
+
+ beforeEach(function() {
+ fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
+ testTags = [
+ new OC.SystemTags.SystemTagModel({id: '1', name: 'test1'}),
+ new OC.SystemTags.SystemTagModel({id: '2', name: 'test2'}),
+ new OC.SystemTags.SystemTagModel({id: '3', name: 'test3'}),
+ ];
+ view.render();
+ });
+ afterEach(function() {
+ fetchStub.restore();
+ });
+ it('grabs values from the full collection', function() {
+ var $el = view.$el.find('input');
+ $el.val('1,3');
+ var opts = select2Stub.getCall(0).args[0];
+ var callback = sinon.stub();
+ opts.initSelection($el, callback);
+
+ expect(fetchStub.calledOnce).toEqual(true);
+ view.collection.add(testTags);
+ fetchStub.yieldTo('success', view.collection);
+
+ expect(callback.calledOnce).toEqual(true);
+ var models = callback.getCall(0).args[0];
+ expect(models.length).toEqual(2);
+ expect(models[0].id).toEqual('1');
+ expect(models[0].name).toEqual('test1');
+ expect(models[1].id).toEqual('3');
+ expect(models[1].name).toEqual('test3');
+ });
+ });
+ describe('tag selection', function() {
+ beforeEach(function() {
+ view.render();
+ var $el = view.$el.find('input');
+ $el.val('1');
+
+ view.collection.add([
+ new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
+ new OC.SystemTags.SystemTagModel({id: '2', name: 'def'}),
+ new OC.SystemTags.SystemTagModel({id: '3', name: 'abd'}),
+ ]);
+ });
+ afterEach(function() {
+ });
+ it('does not create dummy tag when user types non-matching name', function() {
+ var opts = select2Stub.getCall(0).args[0];
+ var result = opts.createSearchChoice('abc');
+ expect(result).not.toBeDefined();
+ });
+ it('creates dummy tag when user types non-matching name', function() {
+ var opts = select2Stub.getCall(0).args[0];
+ var result = opts.createSearchChoice('abnew');
+ expect(result.id).toEqual(-1);
+ expect(result.name).toEqual('abnew');
+ expect(result.isNew).toEqual(true);
+ });
+ it('creates the real tag and fires select event after user selects the dummy tag', function() {
+ var selectHandler = sinon.stub();
+ view.on('select', selectHandler);
+ var createStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'create');
+ view.$el.find('input').trigger(new $.Event('select2-selecting', {
+ object: {
+ id: -1,
+ name: 'newname',
+ isNew: true
+ }
+ }));
+
+ expect(createStub.calledOnce).toEqual(true);
+ expect(createStub.getCall(0).args[0]).toEqual({
+ name: 'newname',
+ userVisible: true,
+ userAssignable: true
+ });
+
+ var newModel = new OC.SystemTags.SystemTagModel({
+ id: '123',
+ name: 'newname',
+ userVisible: true,
+ userAssignable: true
+ });
+
+ // not called yet
+ expect(selectHandler.notCalled).toEqual(true);
+
+ select2Stub.withArgs('data').returns([{
+ id: '1',
+ name: 'abc'
+ }]);
+
+ createStub.yieldTo('success', newModel);
+
+ expect(select2Stub.lastCall.args[0]).toEqual('data');
+ expect(select2Stub.lastCall.args[1]).toEqual([{
+ id: '1',
+ name: 'abc'
+ },
+ newModel.toJSON()
+ ]);
+
+ expect(selectHandler.calledOnce).toEqual(true);
+ expect(selectHandler.getCall(0).args[0]).toEqual(newModel);
+
+ createStub.restore();
+ });
+ it('triggers select event after selecting an existing tag', function() {
+ var selectHandler = sinon.stub();
+ view.on('select', selectHandler);
+ view.$el.find('input').trigger(new $.Event('select2-selecting', {
+ object: {
+ id: '2',
+ name: 'def'
+ }
+ }));
+
+ expect(selectHandler.calledOnce).toEqual(true);
+ expect(selectHandler.getCall(0).args[0]).toEqual(view.collection.get('2'));
+ });
+ it('triggers deselect event after deselecting an existing tag', function() {
+ var selectHandler = sinon.stub();
+ view.on('deselect', selectHandler);
+ view.$el.find('input').trigger(new $.Event('select2-removing', {
+ choice: {
+ id: '2',
+ name: 'def'
+ }
+ }));
+
+ expect(selectHandler.calledOnce).toEqual(true);
+ expect(selectHandler.getCall(0).args[0]).toEqual('2');
+ });
+ });
+ describe('autocomplete', function() {
+ var fetchStub, opts;
+
+ beforeEach(function() {
+ fetchStub = sinon.stub(OC.SystemTags.SystemTagsCollection.prototype, 'fetch');
+ view.render();
+ opts = select2Stub.getCall(0).args[0];
+
+ view.collection.add([
+ new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
+ new OC.SystemTags.SystemTagModel({id: '2', name: 'def'}),
+ new OC.SystemTags.SystemTagModel({id: '3', name: 'abd'}),
+ ]);
+ });
+ afterEach(function() {
+ fetchStub.restore();
+ });
+ it('completes results', function() {
+ var callback = sinon.stub();
+ opts.query({
+ term: 'ab',
+ callback: callback
+ });
+ expect(fetchStub.calledOnce).toEqual(true);
+
+ fetchStub.yieldTo('success', view.collection);
+
+ expect(callback.calledOnce).toEqual(true);
+ expect(callback.getCall(0).args[0].results).toEqual([
+ {
+ id: '1',
+ name: 'abc',
+ userVisible: true,
+ userAssignable: true
+ },
+ {
+ id: '3',
+ name: 'abd',
+ userVisible: true,
+ userAssignable: true
+ }
+ ]);
+ });
+ });
+ describe('tag actions', function() {
+ var $dropdown, opts;
+
+ beforeEach(function() {
+ $dropdown = $('');
+ select2Stub.withArgs('dropdown').returns($dropdown);
+ $('#testArea').append($dropdown);
+
+ view.render();
+
+ opts = select2Stub.getCall(0).args[0];
+
+ view.collection.add([
+ new OC.SystemTags.SystemTagModel({id: '1', name: 'abc'}),
+ ]);
+
+ $dropdown.append(opts.formatResult(view.collection.get('1').toJSON()));
+
+ });
+ afterEach(function() {
+ });
+ it('displays rename form when clicking rename', function() {
+ $dropdown.find('.rename').mouseup();
+ expect($dropdown.find('form.systemtags-rename-form').length).toEqual(1);
+ expect($dropdown.find('form.systemtags-rename-form input').val()).toEqual('abc');
+ });
+ it('renames model and submits change when submitting form', function() {
+ var saveStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'save');
+ $dropdown.find('.rename').mouseup();
+ $dropdown.find('form input').val('abc_renamed');
+ $dropdown.find('form').trigger(new $.Event('submit'));
+
+ expect(saveStub.calledOnce).toEqual(true);
+ expect(saveStub.getCall(0).args[0]).toEqual({'name': 'abc_renamed'});
+
+ expect($dropdown.find('.label').text()).toEqual('abc_renamed');
+ expect($dropdown.find('form').length).toEqual(0);
+
+ saveStub.restore();
+ });
+ it('deletes model and submits change when clicking delete', function() {
+ var destroyStub = sinon.stub(OC.SystemTags.SystemTagModel.prototype, 'destroy');
+
+ expect($dropdown.find('.delete').length).toEqual(0);
+ $dropdown.find('.rename').mouseup();
+ // delete button appears
+ expect($dropdown.find('.delete').length).toEqual(1);
+ $dropdown.find('.delete').mouseup();
+
+ expect(destroyStub.calledOnce).toEqual(true);
+ expect(destroyStub.calledOn(view.collection.get('1')));
+
+ destroyStub.restore();
+ });
+ });
+ describe('setting data', function() {
+ beforeEach(function() {
+ view.render();
+ });
+ it('sets value when calling setValues', function() {
+ var vals = ['1', '2'];
+ view.setValues(vals);
+ expect(select2Stub.lastCall.args[0]).toEqual('val');
+ expect(select2Stub.lastCall.args[1]).toEqual(vals);
+ });
+ it('sets data when calling setData', function() {
+ var vals = [{id: '1', name: 'test1'}, {id: '2', name: 'test2'}];
+ view.setData(vals);
+ expect(select2Stub.lastCall.args[0]).toEqual('data');
+ expect(select2Stub.lastCall.args[1]).toEqual(vals);
+ });
+ });
+});
diff --git a/core/shipped.json b/core/shipped.json
index 28e99c4feb..5dd8700bf1 100644
--- a/core/shipped.json
+++ b/core/shipped.json
@@ -28,6 +28,7 @@
"password_policy",
"provisioning_api",
"sharepoint",
+ "systemtags",
"templateeditor",
"updater",
"user_external",
diff --git a/tests/karma.config.js b/tests/karma.config.js
index df09ee1b31..467b270b35 100644
--- a/tests/karma.config.js
+++ b/tests/karma.config.js
@@ -82,6 +82,16 @@ module.exports = function(config) {
],
testFiles: ['apps/files_versions/tests/js/**/*.js']
},
+ {
+ name: 'systemtags',
+ srcFiles: [
+ // need to enforce loading order...
+ 'apps/systemtags/js/app.js',
+ 'apps/systemtags/js/systemtagsinfoview.js',
+ 'apps/systemtags/js/filesplugin.js'
+ ],
+ testFiles: ['apps/systemtags/tests/js/**/*.js']
+ },
{
name: 'settings',
srcFiles: [