//
// 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)
- 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.url)
- 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.error = m.stream('')
+ 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)
+ vnode.state.error('')
+ }).catch(function(err) {
+ errMsg = err["errors"].join(', ')
+ vnode.state.error('Error: ' + errMsg)
+ })
+ }
+ }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
+ m('span', {
+ style: {
+ color: '#ff0000',
+ margin: '0px 10px'
}
- }, " Edit "),
+ }, [ vnode.state.error() ])
]),
// 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
+}