diff options
author | Roeland Jago Douma <roeland@famdouma.nl> | 2020-05-26 22:56:40 +0300 |
---|---|---|
committer | npmbuildbot[bot] <npmbuildbot[bot]@users.noreply.github.com> | 2020-05-28 22:05:09 +0300 |
commit | 28f8a445e4b7db6dac0f8090d8344d1b3fb7872d (patch) | |
tree | 9f008561e6d52d7bdb37b9c11d3fcad0646a4f40 /core/src | |
parent | b638c6ee09f77e9372fad07899adbb46a6551f7d (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.js | 7 | ||||
-rw-r--r-- | core/src/systemtags/systemtagmodel.js | 60 | ||||
-rw-r--r-- | core/src/systemtags/systemtags.js | 54 | ||||
-rw-r--r-- | core/src/systemtags/systemtagscollection.js | 89 | ||||
-rw-r--r-- | core/src/systemtags/systemtagsinputfield.js | 448 | ||||
-rw-r--r-- | core/src/systemtags/systemtagsmappingcollection.js | 86 | ||||
-rw-r--r-- | core/src/systemtags/templates/result.handlebars | 13 | ||||
-rw-r--r-- | core/src/systemtags/templates/result_form.handlebars | 7 | ||||
-rw-r--r-- | core/src/systemtags/templates/selection.handlebars | 5 |
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}} |