Welcome to mirror list, hosted at ThFree Co, Russian Federation.

github.com/nextcloud/server.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
path: root/core/src
diff options
context:
space:
mode:
authorRoeland Jago Douma <roeland@famdouma.nl>2020-05-26 22:56:40 +0300
committernpmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>2020-05-28 22:05:09 +0300
commit28f8a445e4b7db6dac0f8090d8344d1b3fb7872d (patch)
tree9f008561e6d52d7bdb37b9c11d3fcad0646a4f40 /core/src
parentb638c6ee09f77e9372fad07899adbb46a6551f7d (diff)
Move the systemtags to core/src
* Move to the handlebar loader from webpack Signed-off-by: Roeland Jago Douma <roeland@famdouma.nl> Signed-off-by: npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com>
Diffstat (limited to 'core/src')
-rw-r--r--core/src/systemtags/merged-systemtags.js7
-rw-r--r--core/src/systemtags/systemtagmodel.js60
-rw-r--r--core/src/systemtags/systemtags.js54
-rw-r--r--core/src/systemtags/systemtagscollection.js89
-rw-r--r--core/src/systemtags/systemtagsinputfield.js448
-rw-r--r--core/src/systemtags/systemtagsmappingcollection.js86
-rw-r--r--core/src/systemtags/templates/result.handlebars13
-rw-r--r--core/src/systemtags/templates/result_form.handlebars7
-rw-r--r--core/src/systemtags/templates/selection.handlebars5
9 files changed, 769 insertions, 0 deletions
diff --git a/core/src/systemtags/merged-systemtags.js b/core/src/systemtags/merged-systemtags.js
new file mode 100644
index 00000000000..011544baa08
--- /dev/null
+++ b/core/src/systemtags/merged-systemtags.js
@@ -0,0 +1,7 @@
+import './systemtags.js'
+import './systemtagmodel.js'
+import './systemtagsmappingcollection.js'
+import './systemtagscollection.js'
+import './systemtagsinputfield.js'
+
+import '../../css/systemtags.scss'
diff --git a/core/src/systemtags/systemtagmodel.js b/core/src/systemtags/systemtagmodel.js
new file mode 100644
index 00000000000..b3490c0ddfe
--- /dev/null
+++ b/core/src/systemtags/systemtagmodel.js
@@ -0,0 +1,60 @@
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+(function(OC) {
+
+ _.extend(OC.Files.Client, {
+ PROPERTY_FILEID: '{' + OC.Files.Client.NS_OWNCLOUD + '}id',
+ PROPERTY_CAN_ASSIGN: '{' + OC.Files.Client.NS_OWNCLOUD + '}can-assign',
+ PROPERTY_DISPLAYNAME: '{' + OC.Files.Client.NS_OWNCLOUD + '}display-name',
+ PROPERTY_USERVISIBLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-visible',
+ PROPERTY_USERASSIGNABLE: '{' + OC.Files.Client.NS_OWNCLOUD + '}user-assignable',
+ })
+
+ /**
+ * @class OCA.SystemTags.SystemTagsCollection
+ * @classdesc
+ *
+ * System tag
+ *
+ */
+ const SystemTagModel = OC.Backbone.Model.extend(
+ /** @lends OCA.SystemTags.SystemTagModel.prototype */ {
+ sync: OC.Backbone.davSync,
+
+ defaults: {
+ userVisible: true,
+ userAssignable: true,
+ canAssign: true,
+ },
+
+ davProperties: {
+ 'id': OC.Files.Client.PROPERTY_FILEID,
+ 'name': OC.Files.Client.PROPERTY_DISPLAYNAME,
+ 'userVisible': OC.Files.Client.PROPERTY_USERVISIBLE,
+ 'userAssignable': OC.Files.Client.PROPERTY_USERASSIGNABLE,
+ // read-only, effective permissions computed by the server,
+ 'canAssign': OC.Files.Client.PROPERTY_CAN_ASSIGN,
+ },
+
+ parse: function(data) {
+ return {
+ id: data.id,
+ name: data.name,
+ userVisible: data.userVisible === true || data.userVisible === 'true',
+ userAssignable: data.userAssignable === true || data.userAssignable === 'true',
+ canAssign: data.canAssign === true || data.canAssign === 'true',
+ }
+ },
+ })
+
+ OC.SystemTags = OC.SystemTags || {}
+ OC.SystemTags.SystemTagModel = SystemTagModel
+})(OC)
diff --git a/core/src/systemtags/systemtags.js b/core/src/systemtags/systemtags.js
new file mode 100644
index 00000000000..676f1b8de73
--- /dev/null
+++ b/core/src/systemtags/systemtags.js
@@ -0,0 +1,54 @@
+/* eslint-disable */
+/*
+ * Copyright (c) 2016
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+import escapeHTML from 'escape-html'
+
+(function(OC) {
+ /**
+ * @namespace
+ */
+ OC.SystemTags = {
+ /**
+ *
+ * @param {OC.SystemTags.SystemTagModel|Object|String} tag
+ * @returns {jQuery}
+ */
+ getDescriptiveTag: function(tag) {
+ if (_.isUndefined(tag.name) && !_.isUndefined(tag.toJSON)) {
+ tag = tag.toJSON()
+ }
+
+ if (_.isUndefined(tag.name)) {
+ return $('<span>').addClass('non-existing-tag').text(
+ t('core', 'Non-existing tag #{tag}', {
+ tag: tag
+ })
+ )
+ }
+
+ var $span = $('<span>')
+ $span.append(escapeHTML(tag.name))
+
+ var scope
+ if (!tag.userAssignable) {
+ scope = t('core', 'restricted')
+ }
+ if (!tag.userVisible) {
+ // invisible also implicitly means not assignable
+ scope = t('core', 'invisible')
+ }
+ if (scope) {
+ $span.append($('<em>').text(' (' + scope + ')'))
+ }
+ return $span
+ }
+ }
+})(OC)
diff --git a/core/src/systemtags/systemtagscollection.js b/core/src/systemtags/systemtagscollection.js
new file mode 100644
index 00000000000..fe3f2868558
--- /dev/null
+++ b/core/src/systemtags/systemtagscollection.js
@@ -0,0 +1,89 @@
+/* eslint-disable */
+/*
+ * 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).toLowerCase() === term.toLowerCase()
+ }
+
+ /**
+ * @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/src/systemtags/systemtagsinputfield.js b/core/src/systemtags/systemtagsinputfield.js
new file mode 100644
index 00000000000..d05ac5899fd
--- /dev/null
+++ b/core/src/systemtags/systemtagsinputfield.js
@@ -0,0 +1,448 @@
+/* eslint-disable */
+/*
+ * Copyright (c) 2015
+ *
+ * This file is licensed under the Affero General Public License version 3
+ * or later.
+ *
+ * See the COPYING-README file.
+ *
+ */
+
+import templateResult from './templates/result.handlebars'
+import templateResultForm from './templates/result_form.handlebars'
+import templateSelection from './templates/selection.handlebars'
+
+(function(OC) {
+
+ /**
+ * @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,
+
+ _lastUsedTags: [],
+
+ className: 'systemTagsInputFieldContainer',
+
+ template: function(data) {
+ return '<input class="systemTagsInputField" type="hidden" name="tags" value=""/>'
+ },
+
+ /**
+ * 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 {bool} [options.isAdmin=true] whether the user is an administrator
+ * @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
+ this._isAdmin = !!options.isAdmin
+
+ 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)
+ })
+
+ _.defer(_.bind(this._getLastUsedTags, this))
+
+ _.bindAll(
+ this,
+ '_refreshSelection',
+ '_onClickRenameTag',
+ '_onClickDeleteTag',
+ '_onSelectTag',
+ '_onDeselectTag',
+ '_onSubmitRenameTag'
+ )
+ },
+
+ _getLastUsedTags: function() {
+ var self = this
+ $.ajax({
+ type: 'GET',
+ url: OC.generateUrl('/apps/systemtags/lastused'),
+ success: function(response) {
+ self._lastUsedTags = response
+ }
+ })
+ },
+
+ /**
+ * 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)
+
+ var oldName = tagModel.get('name')
+ var $renameForm = $(templateResultForm({
+ cid: this.cid,
+ name: oldName,
+ deleteTooltip: t('core', 'Delete'),
+ renameLabel: t('core', 'Rename'),
+ isAdmin: this._isAdmin
+ }))
+ $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().trim()
+ 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()
+ $(ev.target).tooltip('hide')
+ $item.closest('.select2-result').remove()
+ // TODO: spinner
+ return false
+ },
+
+ _addToSelect2Selection: function(selection) {
+ var data = this.$tagsField.select2('data')
+ data.push(selection)
+ this.$tagsField.select2('data', data)
+ },
+
+ /**
+ * 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.trim(),
+ userVisible: true,
+ userAssignable: true,
+ canAssign: true
+ }, {
+ success: function(model) {
+ self._addToSelect2Selection(model.toJSON())
+ self._lastUsedTags.unshift(model.id)
+ self.trigger('select', model)
+ },
+ error: function(model, xhr) {
+ if (xhr.status === 409) {
+ // re-fetch collection to get the missing tag
+ self.collection.reset()
+ self.collection.fetch({
+ success: function(collection) {
+ // find the tag in the collection
+ var model = collection.where({
+ name: e.object.name.trim(),
+ userVisible: true,
+ userAssignable: true
+ })
+ if (model.length) {
+ model = model[0]
+ // the tag already exists or was already assigned,
+ // add it to the list anyway
+ self._addToSelect2Selection(model.toJSON())
+ self.trigger('select', model)
+ }
+ }
+ })
+ }
+ }
+ })
+ this.$tagsField.select2('close')
+ e.preventDefault()
+ return false
+ } else {
+ tag = this.collection.get(e.object.id)
+ this._lastUsedTags.unshift(tag.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(collection) {
+ var tagModels = collection.filterByName(query.term.trim())
+ if (!self._isAdmin) {
+ tagModels = _.filter(tagModels, function(tagModel) {
+ return tagModel.get('canAssign')
+ })
+ }
+ query.callback({
+ results: _.invoke(tagModels, 'toJSON')
+ })
+ }
+ })
+ },
+
+ _preventDefault: function(e) {
+ e.stopPropagation()
+ },
+
+ /**
+ * Formats a single dropdown result
+ *
+ * @param {Object} data data to format
+ * @returns {string} HTML markup
+ */
+ _formatDropDownResult: function(data) {
+ return templateResult(_.extend({
+ renameTooltip: t('core', 'Rename'),
+ allowActions: this._allowActions,
+ tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
+ isAdmin: this._isAdmin
+ }, data))
+ },
+
+ /**
+ * Formats a single selection item
+ *
+ * @param {Object} data data to format
+ * @returns {string} HTML markup
+ */
+ _formatSelection: function(data) {
+ return templateSelection(_.extend({
+ tagMarkup: this._isAdmin ? OC.SystemTags.getDescriptiveTag(data)[0].innerHTML : null,
+ isAdmin: this._isAdmin
+ }, data))
+ },
+
+ /**
+ * Create new dummy choice for select2 when the user
+ * types an arbitrary string
+ *
+ * @param {string} term entered term
+ * @returns {Object} dummy tag
+ */
+ _createSearchChoice: function(term) {
+ term = term.trim()
+ if (this.collection.filter(function(entry) {
+ return entry.get('name') === term
+ }).length) {
+ return
+ }
+ if (!this._newTag) {
+ this._newTag = {
+ id: -1,
+ name: term,
+ userAssignable: true,
+ userVisible: true,
+ canAssign: true,
+ isNew: true
+ }
+ } else {
+ this._newTag.name = term
+ }
+
+ return this._newTag
+ },
+
+ _initSelection: function(element, callback) {
+ var self = this
+ var ids = $(element).val().split(',')
+
+ function modelToSelection(model) {
+ var data = model.toJSON()
+ if (!self._isAdmin && !data.canAssign) {
+ // lock static tags for non-admins
+ data.locked = true
+ }
+ return data
+ }
+
+ function findSelectedObjects(ids) {
+ var selectedModels = self.collection.filter(function(model) {
+ return ids.indexOf(model.id) >= 0 && (self._isAdmin || model.get('userVisible'))
+ })
+ return _.map(selectedModels, modelToSelection)
+ }
+
+ 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', 'Collaborative 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: _.bind(this._formatSelection, this),
+ 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) {
+ var aLastUsed = self._lastUsedTags.indexOf(a.id)
+ var bLastUsed = self._lastUsedTags.indexOf(b.id)
+
+ if (aLastUsed !== bLastUsed) {
+ if (bLastUsed === -1) {
+ return -1
+ }
+ if (aLastUsed === -1) {
+ return 1
+ }
+ return aLastUsed < bLastUsed ? -1 : 1
+ }
+
+ // Both not found
+ return OC.Util.naturalSortCompare(a.name, b.name)
+ }
+ if (aSelected && !bSelected) {
+ return -1
+ }
+ return 1
+ })
+ return results
+ },
+ formatNoMatches: function() {
+ return t('core', 'No tags found')
+ }
+ })
+ .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')
+ }
+ },
+
+ getValues: function() {
+ this.$tagsField.select2('val')
+ },
+
+ 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/src/systemtags/systemtagsmappingcollection.js b/core/src/systemtags/systemtagsmappingcollection.js
new file mode 100644
index 00000000000..834ba57f945
--- /dev/null
+++ b/core/src/systemtags/systemtagsmappingcollection.js
@@ -0,0 +1,86 @@
+/*
+ * 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
+ *
+ */
+ const 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/src/systemtags/templates/result.handlebars b/core/src/systemtags/templates/result.handlebars
new file mode 100644
index 00000000000..faa7cc4d348
--- /dev/null
+++ b/core/src/systemtags/templates/result.handlebars
@@ -0,0 +1,13 @@
+<span class="systemtags-item{{#if isNew}} new-item{{/if}}" data-id="{{id}}">
+<span class="checkmark icon icon-checkmark"></span>
+ {{#if isAdmin}}
+ <span class="label">{{{tagMarkup}}}</span>
+ {{else}}
+ <span class="label">{{name}}</span>
+ {{/if}}
+ {{#allowActions}}
+ <span class="systemtags-actions">
+ <a href="#" class="rename icon icon-rename" title="{{renameTooltip}}"></a>
+ </span>
+ {{/allowActions}}
+</span>
diff --git a/core/src/systemtags/templates/result_form.handlebars b/core/src/systemtags/templates/result_form.handlebars
new file mode 100644
index 00000000000..28fe8c56fe2
--- /dev/null
+++ b/core/src/systemtags/templates/result_form.handlebars
@@ -0,0 +1,7 @@
+<form class="systemtags-rename-form">
+ <label class="hidden-visually" for="{{cid}}-rename-input">{{renameLabel}}</label>
+ <input id="{{cid}}-rename-input" type="text" value="{{name}}">
+ {{#if isAdmin}}
+ <a href="#" class="delete icon icon-delete" title="{{deleteTooltip}}"></a>
+ {{/if}}
+</form>
diff --git a/core/src/systemtags/templates/selection.handlebars b/core/src/systemtags/templates/selection.handlebars
new file mode 100644
index 00000000000..b006b129748
--- /dev/null
+++ b/core/src/systemtags/templates/selection.handlebars
@@ -0,0 +1,5 @@
+{{#if isAdmin}}
+ <span class="label">{{{tagMarkup}}}</span>
+{{else}}
+ <span class="label">{{name}}</span>
+{{/if}}