//
// SPDX-License-Identifier: AGPL-3.0
+window.SimpleInput = {
+ view: function(vnode) {
+ return m('input.form-control', {
+ style: {
+ width: '100%',
+ },
+ type: 'text',
+ placeholder: 'Add ' + vnode.attrs.placeholder,
+ value: vnode.attrs.value,
+ onchange: function() {
+ if (this.value != '') {
+ vnode.attrs.value(this.value)
+ }
+ },
+ }, vnode.attrs.value)
+ },
+}
+
window.SelectOrAutocomplete = {
view: function(vnode) {
- return m("input", {
+ return m('input.form-control', {
style: {
width: '100%'
},
type: 'text',
- value: vnode.attrs.value
+ value: vnode.attrs.value,
+ placeholder: (vnode.attrs.create ? 'Add or select ': 'Select ') + vnode.attrs.placeholder,
}, vnode.attrs.value)
},
oncreate: function(vnode) {
- this.selectized = $(vnode.dom).selectize({
- labelField: 'value',
- valueField: 'value',
- searchField: 'value',
- sortField: 'value',
- maxItems: 1,
- placeholder: vnode.attrs.placeholder,
- create: vnode.attrs.create ? function(input) {
- return {value: input}
- } : false,
- items: [vnode.attrs.value()],
- options: vnode.attrs.options.map(function(option) {
- return {value: option}
- }),
- onChange: function(val) {
- if (val != '') {
- vnode.attrs.value(val)
+ vnode.state.awesomplete = new Awesomplete(vnode.dom, {
+ list: vnode.attrs.options,
+ minChars: 0,
+ maxItems: 1000000,
+ autoFirst: true,
+ sort: false,
+ })
+ 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)
}
}
})
- if (vnode.attrs.setFocus) {
- this.selectized[0].selectize.focus()
- }
- }
+ $(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.TagEditorRow = {
view: function(vnode) {
- // Name options list
var nameOpts = Object.keys(vnode.attrs.vocabulary().tags)
- if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().tags)) {
- nameOpts.push(vnode.attrs.name())
- }
- // Value options list
var valueOpts = []
- 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
- }
- if (vnode.attrs.value() != '') {
- valueOpts.push(vnode.attrs.value())
+ 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
+ }
}
- return m("tr", [
+ return m('tr', [
// Erase tag
- m("td", [
+ m('td', [
vnode.attrs.editMode &&
m('div.text-center', m('a.btn.btn-default.btn-sm', {
style: {
onclick: function(e) { vnode.attrs.removeTag() }
}, m('i.fa.fa-fw.fa-trash-o')))
]),
- // Tag name
- m("td", [
+ // Tag key
+ m('td', [
vnode.attrs.editMode ?
- m("div", {key: 'name-'+vnode.attrs.name()},[
- m(SelectOrAutocomplete, {
+ m('div', {key: 'key'}, [
+ m(inputComponent, {
options: nameOpts,
value: vnode.attrs.name,
- // Allow any tag name unless "strict" is set to true.
+ // Allow any tag name unless 'strict' is set to true.
create: !vnode.attrs.vocabulary().strict,
- placeholder: 'new tag',
- // Focus on tag name field when adding a new tag that's
- // not the first one.
- setFocus: !vnode.attrs.firstRow && vnode.attrs.name() === ''
+ placeholder: 'key',
})
])
: vnode.attrs.name
]),
// Tag value
- m("td", [
+ m('td', [
vnode.attrs.editMode ?
- m("div", {key: 'value-'+vnode.attrs.name()}, [
- m(SelectOrAutocomplete, {
+ m('div', {key: 'value'}, [
+ m(inputComponent, {
options: valueOpts,
value: vnode.attrs.value,
- placeholder: 'new 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.
|| !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,
- // Focus on tag value field when new tag name is set
- setFocus: vnode.attrs.name() !== '' && vnode.attrs.value() === ''
})
])
: vnode.attrs.value
window.TagEditorTable = {
view: function(vnode) {
- return m("table.table.table-condensed", [
- 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", [
+ m('tbody', [
vnode.attrs.tags.length > 0
? vnode.attrs.tags.map(function(tag, idx) {
return m(TagEditorRow, {
- key: idx,
+ key: tag.rowKey,
removeTag: function() {
vnode.attrs.tags.splice(idx, 1)
vnode.attrs.dirty(true)
},
editMode: vnode.attrs.editMode,
- firstRow: vnode.attrs.tags.length === 1,
name: tag.name,
value: tag.value,
vocabulary: vnode.attrs.vocabulary
})
})
- : m("tr", m("td[colspan=3]", m("center","loading tags...")))
+ : 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)}
+ 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(v) {
- if (v !== '') {
- vnode.state.dirty(true)
- }
- })
- tag.value.map(function(v) {
- if (v !== '') {
- vnode.state.dirty(true)
+ tag.name.map(function() { vnode.state.dirty(true) })
+ tag.value.map(function() { vnode.state.dirty(true) })
+ tag.name.map(m.redraw)
+ },
+ fixTag: function(vnode, tagName) {
+ // Recover tag if deleted, recover its value if modified
+ savedTagValue = vnode.state.saved_tags[tagName]
+ if (savedTagValue === undefined) {
+ return
+ }
+ found = false
+ vnode.state.tags.forEach(function(tag) {
+ if (tag.name == tagName) {
+ tag.value = vnode.state.saved_tags[tagName]
+ found = true
}
})
- tag.name.map(m.redraw)
+ if (!found) {
+ vnode.state.tags.pop() // Remove the last empty row
+ vnode.state.appendTag(vnode, tagName, savedTagValue)
+ }
},
oninit: function(vnode) {
vnode.state.sessionDB = new SessionDB()
// Get vocabulary
- vnode.state.vocabulary = m.stream({"strict":false, "tags":{}})
- m.request('/vocabulary.json').then(vnode.state.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.saved_tags = {}
vnode.state.dirty = m.stream(false)
vnode.state.dirty.map(m.redraw)
- vnode.state.objPath = '/arvados/v1/'+vnode.attrs.targetController+'/'+vnode.attrs.targetUuid
+ 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,
+ 'arvados/v1/' + vnode.attrs.targetController,
{
data: {
filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
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)
+ vnode.state.saved_tags = o.properties
// 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() !== '')) {
+ if (lastTag === undefined || (lastTag.name() !== '' || lastTag.value() !== '')) {
vnode.state.appendTag(vnode, '', '')
}
})
view: function(vnode) {
return [
vnode.state.editMode &&
- m("div.pull-left", [
- m("a.btn.btn-primary.btn-sm"+(vnode.state.dirty() ? '' : '.disabled'), {
+ m('div.pull-left', [
+ m('a.btn.btn-primary.btn-sm' + (vnode.state.dirty() ? '' : '.disabled'), {
style: {
margin: '10px 0px'
},
onclick: function(e) {
var tags = {}
vnode.state.tags.forEach(function(t) {
- if (t.name() != '' && t.value() != '') {
+ // 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",
+ method: 'PUT',
data: {properties: JSON.stringify(tags)}
}
).then(function(v) {
vnode.state.dirty(false)
+ vnode.state.error('')
+ vnode.state.saved_tags = tags
+ }).catch(function(err) {
+ if (err.errors !== undefined) {
+ var re = /protected\ property/i
+ var protected_props = []
+ err.errors.forEach(function(error) {
+ if (re.test(error)) {
+ prop = error.split(':')[1].trim()
+ vnode.state.fixTag(vnode, prop)
+ protected_props.push(prop)
+ }
+ })
+ if (protected_props.length > 0) {
+ errMsg = "Protected properties cannot be updated: " + protected_props.join(', ')
+ } else {
+ errMsg = errors.join(', ')
+ }
+ } else {
+ errMsg = err
+ }
+ vnode.state.error(errMsg)
})
}
- }, vnode.state.dirty() ? ' Save changes ' : ' Saved ')
+ }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
+ m('span', {
+ style: {
+ color: '#ff0000',
+ margin: '0px 10px'
+ }
+ }, [ vnode.state.error() ])
]),
// Tags table
m(TagEditorTable, {
})
]
},
-}
\ No newline at end of file
+}