Fix 2.4.2 upgrade notes formatting refs #19330
[arvados.git] / apps / workbench / app / assets / javascripts / components / edit_tags.js
index 1983c3b35586b9f2132cdbb417cefbe754e1bcce..5e02279ea19ca8b1ef3721d7b56078e09d9f4096 100644 (file)
 //
 // 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) {
-        $(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)
                 }
             }
         })
-    }
+        $(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().types)
-        if (vnode.attrs.name() != '' && !(vnode.attrs.name() in vnode.attrs.vocabulary().types)) {
-            nameOpts.push(vnode.attrs.name())
-        }
-        // Value options list
+        var nameOpts = Object.keys(vnode.attrs.vocabulary().tags)
         var valueOpts = []
-        if (vnode.attrs.name() in vnode.attrs.vocabulary().types &&
-            'options' in vnode.attrs.vocabulary().types[vnode.attrs.name()]) {
-                valueOpts = vnode.attrs.vocabulary().types[vnode.attrs.name()].options
-        }
-        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",
-            vnode.attrs.editMode &&
+            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 name
-            m("td",
-            vnode.attrs.editMode ?
-            m("div", {key: 'name-'+vnode.attrs.name()},[m(SelectOrAutocomplete, {
-                options: nameOpts,
-                value: vnode.attrs.name,
-                create: !vnode.attrs.vocabulary().strict,
-                placeholder: 'new tag',
-            })])
-            : vnode.attrs.name),
+            ]),
+            // 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',
+                    })
+                ])
+                : vnode.attrs.name
+            ]),
             // Tag value
-            m("td",
-            vnode.attrs.editMode ?
-            m("div", {key: 'value-'+vnode.attrs.name()}, [m(SelectOrAutocomplete, {
-                options: valueOpts,
-                value: vnode.attrs.value,
-                placeholder: 'new value',
-                create: (vnode.attrs.name() in vnode.attrs.vocabulary().types)
-                    ? (vnode.attrs.vocabulary().types[vnode.attrs.name()].type == 'text') || 
-                        vnode.attrs.vocabulary().types[vnode.attrs.name()].overridable || false
-                    : true, // If tag not in vocabulary, we should accept any 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,
+                    })
+                ])
+                : vnode.attrs.value
             ])
-            : vnode.attrs.value)
         ])
     }
 }
 
 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", [
+            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)
@@ -123,34 +165,58 @@ window.TagEditorTable = {
                         vocabulary: vnode.attrs.vocabulary
                     })
                 })
-                : m("tr", m("td[colspan=3]", m("center","(no 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)}
-        tag.name.map(vnode.state.dirty)
-        tag.value.map(vnode.state.dirty)
-        tag.name.map(m.redraw)
+        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)
+    },
+    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
+            }
+        })
+        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, "types":{}})
-        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]]),
@@ -162,8 +228,20 @@ 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() !== '')) {
+                            vnode.state.appendTag(vnode, '', '')
+                        }
+                    })
                 }
             }
         )
@@ -171,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() === false) ? '' : '.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() === false) ? ' Save changes ' : ' Saved ')
+                }, vnode.state.dirty() ? ' Save changes ' : ' Saved '),
+                m('span', {
+                    style: {
+                        color: '#ff0000',
+                        margin: '0px 10px'
+                    }
+                }, [ vnode.state.error() ])
             ]),
             // Tags table
             m(TagEditorTable, {
@@ -201,19 +308,7 @@ window.TagEditorApp = {
                 tags: vnode.state.tags,
                 vocabulary: vnode.state.vocabulary,
                 dirty: vnode.state.dirty
-            }),
-            vnode.state.editMode &&
-            m("div.pull-left", [
-                // Add tag button
-                m("a.btn.btn-primary.btn-sm", {
-                    onclick: function(e) {
-                        vnode.state.appendTag(vnode, '', '')
-                    }
-                }, [
-                    m("i.glyphicon.glyphicon-plus"),
-                    " Add new tag "
-                ])
-            ])
+            })
         ]
     },
-}
\ No newline at end of file
+}