X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/b8dccb1fa84d699e1220e1a3d57b13085349b5ce..08123387faf00b0af64ea01f7196d8c5ca3fae1e:/apps/workbench/app/assets/javascripts/components/edit_tags.js diff --git a/apps/workbench/app/assets/javascripts/components/edit_tags.js b/apps/workbench/app/assets/javascripts/components/edit_tags.js index 0c844d4d6c..1fddb2651e 100644 --- a/apps/workbench/app/assets/javascripts/components/edit_tags.js +++ b/apps/workbench/app/assets/javascripts/components/edit_tags.js @@ -2,290 +2,264 @@ // // SPDX-License-Identifier: AGPL-3.0 -window.IntegerField = { +window.SimpleInput = { view: function(vnode) { - var tags = vnode.attrs.tags - var voc = vnode.attrs.voc - var tagName = tags.getName(vnode.attrs.tagIdx) - var tagDef = voc.getDef(tagName) - var min = tagDef.min || false - var max = tagDef.max || false - return m("input", { - type: 'number', + return m('input.form-control', { style: { width: '100%', }, - oninput: m.withAttr("value", function(val) { - // Validations - if (isNaN(parseInt(val))) { return } - if (min && val < min) { return } - if (max && val > max) { return } - // Value accepted - tags.data[vnode.attrs.tagIdx]["value"] = parseInt(val) - }), - value: tags.data[vnode.attrs.tagIdx]["value"] - }, tags.data[vnode.attrs.tagIdx]["value"]) - } -} - -window.TextField = { - view: function(vnode) { - var tags = vnode.attrs.tags - var voc = vnode.attrs.voc - var tagName = tags.getName(vnode.attrs.tagIdx) - var tagDef = voc.getDef(tagName) - var max_length = tagDef.max_length || false - return m("input", { type: 'text', - style: { - width: '100%', + placeholder: 'Add ' + vnode.attrs.placeholder, + value: vnode.attrs.value, + onchange: function() { + if (this.value != '') { + vnode.attrs.value(this.value) + } }, - oninput: m.withAttr("value", function(val) { - // Validation - if (max_length && val.length > max_length) { return } - // Value accepted - tags.data[vnode.attrs.tagIdx]["value"] = val - }), - value: tags.data[vnode.attrs.tagIdx]["value"] - }, tags.data[vnode.attrs.tagIdx]["value"]) - } + }, vnode.attrs.value) + }, } -window.SelectNameField = { +window.SelectOrAutocomplete = { view: function(vnode) { - return m("input[type=text]", { + return m('input.form-control', { style: { width: '100%' }, - }) + type: 'text', + value: vnode.attrs.value, + placeholder: (vnode.attrs.create ? 'Add or select ': 'Select ') + vnode.attrs.placeholder, + }, vnode.attrs.value) }, oncreate: function(vnode) { - var tags = vnode.attrs.tags - var voc = vnode.attrs.voc - var opts = voc.getTypes().map(function(x) { - return { - value: x, - label: x - } + vnode.state.awesomplete = new Awesomplete(vnode.dom, { + list: vnode.attrs.options, + minChars: 0, + maxItems: 1000000, + autoFirst: true, + sort: false, }) - // Tag name not included on vocabulary, add it to the options - var tagName = tags.getName(vnode.attrs.tagIdx) - if (!voc.getTypes().includes(tagName)) { - opts = opts.concat([{value: tagName, label: tagName}]) - } - $(vnode.dom).selectize({ - options: opts, - persist: false, - maxItems: 1, - labelField: 'label', - valueField: 'value', - items: [tags.data[vnode.attrs.tagIdx]["name"]], - create: function(input) { - return { - value: input, - label: input + vnode.state.create = vnode.attrs.create + vnode.state.options = vnode.attrs.options + // Option is selected from the list. + $(vnode.dom).on('awesomplete-selectcomplete', function(event) { + vnode.attrs.value(this.value) + }) + $(vnode.dom).on('change', function(event) { + if (!vnode.state.create && !(this.value in vnode.state.options)) { + this.value = vnode.attrs.value() + } else { + if (vnode.attrs.value() !== this.value) { + vnode.attrs.value(this.value) } - }, - onChange: function(val) { - tags.data[vnode.attrs.tagIdx]["name"] = val - m.redraw() } }) - } + $(vnode.dom).on('focusin', function(event) { + if (this.value === '') { + vnode.state.awesomplete.evaluate() + vnode.state.awesomplete.open() + } + }) + }, + onupdate: function(vnode) { + vnode.state.awesomplete.list = vnode.attrs.options + vnode.state.create = vnode.attrs.create + vnode.state.options = vnode.attrs.options + }, } -window.SelectField = { +window.TagEditorRow = { view: function(vnode) { - var tags = vnode.attrs.tags - var voc = vnode.attrs.voc - var tagName = tags.getName(vnode.attrs.tagIdx) - var overridable = voc.getDef(tagName).overridable || false - var opts = voc.getDef(tagName).options - // If current value isn't listed and it's an overridable type, add - // it to the available options - if (!opts.includes(tags.data[vnode.attrs.tagIdx]["value"]) && - overridable) { - opts = opts.concat([tags.data[vnode.attrs.tagIdx]["value"]]) + var nameOpts = Object.keys(vnode.attrs.vocabulary().tags) + var valueOpts = [] + var inputComponent = SelectOrAutocomplete + if (nameOpts.length === 0) { + // If there's not vocabulary defined, switch to a simple input field + inputComponent = SimpleInput + } else { + // Name options list + if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) { + nameOpts.push(vnode.attrs.name()) + } + // Value options list + if (vnode.attrs.name() in vnode.attrs.vocabulary().tags && + 'values' in vnode.attrs.vocabulary().tags[vnode.attrs.name()]) { + valueOpts = vnode.attrs.vocabulary().tags[vnode.attrs.name()].values + } } - // Wrap the select inside a div element so it can be replaced - return m("div", { - style: { - width: '100%' - }, - }, [ - m("select", { - style: { - width: '100%' - }, - oncreate: function(v) { - $(v.dom).selectize({ - create: overridable, - onChange: function(val) { - tags.data[vnode.attrs.tagIdx]["value"] = val - m.redraw() // 3rd party event handlers need to do this - } + return m('tr', [ + // Erase tag + m('td', [ + vnode.attrs.editMode && + m('div.text-center', m('a.btn.btn-default.btn-sm', { + style: { + align: 'center' + }, + onclick: function(e) { vnode.attrs.removeTag() } + }, m('i.fa.fa-fw.fa-trash-o'))) + ]), + // Tag key + m('td', [ + vnode.attrs.editMode ? + m('div', {key: 'key'}, [ + m(inputComponent, { + options: nameOpts, + value: vnode.attrs.name, + // Allow any tag name unless 'strict' is set to true. + create: !vnode.attrs.vocabulary().strict, + placeholder: 'key', }) - }, - }, - opts.map(function(k) { - return m("option", { - value: k, - selected: tags.data[vnode.attrs.tagIdx]["value"] === k, - }, k) - }) - ) - ]) - } -} - -// Maps tag types against editor components -var typeMap = { - "select": SelectField, - "text": TextField, - "integer": IntegerField -} - -// When in edit mode, present a tag name selector and tag value -// selector/editor depending of the tag type. -window.TagEditor = { - view: function(vnode) { - var tags = vnode.attrs.tags - var voc = vnode.attrs.voc - var tagIdx = vnode.attrs.tagIdx - if (tagIdx in tags.data) { - var tagName = tags.getName(vnode.attrs.tagIdx)//tags.data[tagIdx].name - var tagType = voc.getDef(tagName).type - return m("tr.collection-tag-"+tagName, [ - m("td", - vnode.attrs.editMode() ? - m("i.glyphicon.glyphicon-remove.collection-tag-remove", { - style: "cursor: pointer;", - onclick: function(e) { - // Erase tag - tags.removeTag(tagIdx) - } + ]) + : vnode.attrs.name + ]), + // Tag value + m('td', [ + vnode.attrs.editMode ? + m('div', {key: 'value'}, [ + m(inputComponent, { + options: valueOpts, + value: vnode.attrs.value, + placeholder: 'value', + // Allow any value on tags not listed on the vocabulary. + // Allow any value on tags without values, or the ones + // that aren't explicitly declared to be strict. + create: !(vnode.attrs.name() in vnode.attrs.vocabulary().tags) + || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].values + || vnode.attrs.vocabulary().tags[vnode.attrs.name()].values.length === 0 + || !vnode.attrs.vocabulary().tags[vnode.attrs.name()].strict, }) - : ""), - m("td.collection-tag-field.collection-tag-field-key", - // Tag name - vnode.attrs.editMode() ? - m(SelectNameField, { - tagIdx: tagIdx, - tags: tags, - voc: voc - }) - : tags.data[tagIdx]["name"]), - m("td.collection-tag-field.collection-tag-field-value", - // Tag value - vnode.attrs.editMode() ? - m(typeMap[tagType], { - tagIdx: tagIdx, - tags: tags, - voc: voc - }) - : tags.data[tagIdx]["value"]) + ]) + : vnode.attrs.value ]) - } + ]) } } -window.TagTable = { +window.TagEditorTable = { view: function(vnode) { - return m("table.table.table-condensed", { - border: "1" - }, [ - m("colgroup", [ - m("col", {width:"5%"}), - m("col", {width:"25%"}), - m("col", {width:"70%"}), + return m('table.table.table-condensed.table-justforlayout', [ + m('colgroup', [ + m('col', {width:'5%'}), + m('col', {width:'25%'}), + m('col', {width:'70%'}), ]), - m("thead", [ - m("tr", [ - m("th"), - m("th", "Key"), - m("th", "Value"), + m('thead', [ + m('tr', [ + m('th'), + m('th', 'Key'), + m('th', 'Value'), ]) ]), - m("tbody.collection-tag-rows", [ - Object.keys(vnode.attrs.tags.data).map(function(k) { - return m(TagEditor, { - tagIdx: k, - key: k, + m('tbody', [ + vnode.attrs.tags.length > 0 + ? vnode.attrs.tags.map(function(tag, idx) { + return m(TagEditorRow, { + key: tag.rowKey, + removeTag: function() { + vnode.attrs.tags.splice(idx, 1) + vnode.attrs.dirty(true) + }, editMode: vnode.attrs.editMode, - tags: vnode.attrs.tags, - voc: vnode.attrs.voc + name: tag.name, + value: tag.value, + vocabulary: vnode.attrs.vocabulary }) }) + : m('tr', m('td[colspan=3]', m('center', 'Loading tags...'))) ]), - ] - ) + ]) } } +var uniqueID = 1 + window.TagEditorApp = { + appendTag: function(vnode, name, value) { + var tag = {name: m.stream(name), value: m.stream(value), rowKey: uniqueID++} + vnode.state.tags.push(tag) + // Set dirty flag when any of name/value changes to non empty string + tag.name.map(function() { vnode.state.dirty(true) }) + tag.value.map(function() { vnode.state.dirty(true) }) + tag.name.map(m.redraw) + }, oninit: function(vnode) { vnode.state.sessionDB = new SessionDB() - vnode.state.url = new URL(document.URL) - var pathname = vnode.state.url.pathname.split("/") - vnode.state.uuid = pathname.pop() - vnode.state.objType = pathname.pop() - vnode.state.tags = new Tags(vnode.state.sessionDB, vnode.state.uuid, vnode.state.objType) - vnode.state.tags.load() - vnode.state.vocabulary = new Vocabulary() - vnode.state.vocabulary.load() - vnode.state.editMode = m.stream(false) - vnode.state.tagTable = TagTable + // Get vocabulary + vnode.state.vocabulary = m.stream({'strict':false, 'tags':{}}) + var vocabularyTimestamp = parseInt(Date.now() / 300000) // Bust cache every 5 minutes + m.request('/vocabulary.json?v=' + vocabularyTimestamp).then(vnode.state.vocabulary) + vnode.state.editMode = vnode.attrs.targetEditable + vnode.state.tags = [] + vnode.state.dirty = m.stream(false) + vnode.state.dirty.map(m.redraw) + vnode.state.objPath = 'arvados/v1/' + vnode.attrs.targetController + '/' + vnode.attrs.targetUuid + // Get tags + vnode.state.sessionDB.request( + vnode.state.sessionDB.loadLocal(), + 'arvados/v1/' + vnode.attrs.targetController, + { + data: { + filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]), + select: JSON.stringify(['properties']) + }, + }).then(function(obj) { + if (obj.items.length == 1) { + o = obj.items[0] + Object.keys(o.properties).forEach(function(k) { + vnode.state.appendTag(vnode, k, o.properties[k]) + }) + if (vnode.state.editMode) { + vnode.state.appendTag(vnode, '', '') + } + // Data synced with server, so dirty state should be false + vnode.state.dirty(false) + // Add new tag row when the last one is completed + vnode.state.dirty.map(function() { + if (!vnode.state.editMode) { return } + lastTag = vnode.state.tags.slice(-1).pop() + if (lastTag === undefined || (lastTag.name() !== '' || lastTag.value() !== '')) { + vnode.state.appendTag(vnode, '', '') + } + }) + } + } + ) }, view: function(vnode) { return [ - m("p", [ - // Edit button - m("a.btn.btn-primary"+(vnode.state.editMode() ? '.disabled':''), { + vnode.state.editMode && + m('div.pull-left', [ + m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), { + style: { + margin: '10px 0px' + }, onclick: function(e) { - vnode.state.editMode(true) + var tags = {} + vnode.state.tags.forEach(function(t) { + // Only ignore tags with empty key + if (t.name() != '') { + tags[t.name()] = t.value() + } + }) + vnode.state.sessionDB.request( + vnode.state.sessionDB.loadLocal(), + vnode.state.objPath, { + method: 'PUT', + data: {properties: JSON.stringify(tags)} + } + ).then(function(v) { + vnode.state.dirty(false) + }) } - }, " Edit "), + }, vnode.state.dirty() ? ' Save changes ' : ' Saved ') ]), // Tags table - m(vnode.state.tagTable, { + m(TagEditorTable, { editMode: vnode.state.editMode, tags: vnode.state.tags, - voc: vnode.state.vocabulary - }), - vnode.state.editMode() ? - m("div", [ - m("div.pull-left", [ - // Add tag button - m("a.btn.btn-primary.btn-sm", { - onclick: function(e) { - vnode.state.tags.addTag(vnode.state.vocabulary.getTypes()[0]) - } - }, [ - m("i.glyphicon.glyphicon-plus"), - " Add new tag " - ]) - ]), - m("div.pull-right", [ - // Save button - m("a.btn.btn-primary.btn-sm", { - onclick: function(e) { - vnode.state.editMode(false) - vnode.state.tags.save().then(function() { - vnode.state.tags.load() - }) - } - }, " Save "), - // Cancel button - m("a.btn.btn-primary.btn-sm", { - onclick: function(e) { - vnode.state.editMode(false) - e.redraw = false - vnode.state.tags.load().then(m.redraw()) - } - }, " Cancel ") - ]) - ]) - : "" + vocabulary: vnode.state.vocabulary, + dirty: vnode.state.dirty + }) ] }, -} \ No newline at end of file +}