X-Git-Url: https://git.arvados.org/arvados.git/blobdiff_plain/90dd1c90fb310834234163711019e2b932c6e396..a981ba7008866165a3941707ba2a98df34c424e0:/apps/workbench/app/assets/javascripts/components/edit_tags.js?ds=sidebyside diff --git a/apps/workbench/app/assets/javascripts/components/edit_tags.js b/apps/workbench/app/assets/javascripts/components/edit_tags.js index e4eb2007bb..5e02279ea1 100644 --- a/apps/workbench/app/assets/javascripts/components/edit_tags.js +++ b/apps/workbench/app/assets/javascripts/components/edit_tags.js @@ -4,67 +4,68 @@ window.SimpleInput = { view: function(vnode) { - return m("input.form-control", { + return m('input.form-control', { style: { width: '100%', }, type: 'text', - placeholder: vnode.attrs.placeholder, + placeholder: 'Add ' + vnode.attrs.placeholder, value: vnode.attrs.value, onchange: function() { - console.log(this.value) if (this.value != '') { vnode.attrs.value(this.value) } }, }, vnode.attrs.value) }, - oncreate: function(vnode) { - if (vnode.attrs.setFocus) { - vnode.dom.focus() - } - } } 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', - persist: false, - hideSelected: true, - openOnFocus: false, - createOnBlur: true, - 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 = { @@ -85,13 +86,10 @@ window.TagEditorRow = { '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()) - } } - 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: { @@ -100,31 +98,28 @@ window.TagEditorRow = { 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('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.name() === '' + placeholder: 'key', }) ]) : vnode.attrs.name ]), // Tag value - m("td", [ + m('td', [ vnode.attrs.editMode ? - m("div", {key: 'value-'+vnode.attrs.name()}, [ + 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. @@ -132,8 +127,6 @@ window.TagEditorRow = { || !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 @@ -144,24 +137,24 @@ window.TagEditorRow = { window.TagEditorTable = { view: function(vnode) { - return m("table.table.table-condensed.table-justforlayout", [ - 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) @@ -172,43 +165,58 @@ window.TagEditorTable = { 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]]), @@ -220,13 +228,17 @@ window.TagEditorApp = { 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, '', '') } }) @@ -237,29 +249,58 @@ window.TagEditorApp = { 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, { @@ -270,4 +311,4 @@ window.TagEditorApp = { }) ] }, -} \ No newline at end of file +}