12479: Allow the user to edit the currently accepted value on a
[arvados.git] / apps / workbench / app / assets / javascripts / components / edit_tags.js
index 939692acfebc3b808c6d1b073d3d592cd6bdbad4..fa3f13e4ac7ca000d5db54710065937e6adcffe2 100644 (file)
@@ -2,7 +2,41 @@
 //
 // SPDX-License-Identifier: AGPL-3.0
 
+window.SimpleInput = {
+    view: function(vnode) {
+        return m("input.form-control", {
+            style: {
+                width: '100%',
+            },
+            type: 'text',
+            placeholder: 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 = {
+    onFocus: function(vnode) {
+        // Allow the user to edit an already entered value by removing it
+        // and filling the input field with the same text
+        activeSelect = vnode.state.selectized[0].selectize
+        value = activeSelect.getValue()
+        if (value.length > 0) {
+            activeSelect.clear(silent = true)
+            activeSelect.setTextboxValue(value)
+        }
+    },
     view: function(vnode) {
         return m("input", {
             style: {
@@ -13,12 +47,17 @@ window.SelectOrAutocomplete = {
         }, vnode.attrs.value)
     },
     oncreate: function(vnode) {
-        vnode.state.selector = $(vnode.dom).selectize({
+        vnode.state.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,
@@ -27,69 +66,98 @@ window.SelectOrAutocomplete = {
                 return {value: option}
             }),
             onChange: function(val) {
-                vnode.attrs.value(val)
-                m.redraw()
+                if (val != '') {
+                    vnode.attrs.value(val)
+                }
+            },
+            onFocus: function() {
+                vnode.state.onFocus(vnode)
             }
-        }).data('selectize')
+        })
+        if (vnode.attrs.setFocus) {
+            vnode.state.selectized[0].selectize.focus()
+        }
     }
 }
 
-// When in edit mode, present a tag name selector and tag value
-// selector/editor depending of the tag type.
 window.TagEditorRow = {
     view: function(vnode) {
-        // Value options list
-        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
+        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
+            }
+            if (vnode.attrs.value() != '') {
+                valueOpts.push(vnode.attrs.value())
+            }
         }
-        valueOpts.push(vnode.attrs.value())
-
         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'))),
-            ),
+                }, 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: (vnode.attrs.name() in vnode.attrs.vocabulary().types)
-                    ? Object.keys(vnode.attrs.vocabulary().types)
-                    : Object.keys(vnode.attrs.vocabulary().types).concat(vnode.attrs.name()),
-                value: vnode.attrs.name,
-                create: vnode.attrs.vocabulary().strict
-            })])
-            : vnode.attrs.name),
+            m("td", [
+                vnode.attrs.editMode ?
+                m("div", {key: 'name-'+vnode.attrs.name()},[
+                    m(inputComponent, {
+                        options: nameOpts,
+                        value: vnode.attrs.name,
+                        // 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() === ''
+                    })
+                ])
+                : 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,
-                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-'+vnode.attrs.name()}, [
+                    m(inputComponent, {
+                        options: valueOpts,
+                        value: vnode.attrs.value,
+                        placeholder: 'new 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,
+                        // Focus on tag value field when new tag name is set
+                        setFocus: vnode.attrs.name() !== '' && vnode.attrs.value() === ''
+                    })
+                ])
+                : vnode.attrs.value
             ])
-            : vnode.attrs.value)
         ])
     }
 }
 
 window.TagEditorTable = {
     view: function(vnode) {
-        return m("table.table.table-condensed", {
-            border: "1"
-        }, [
+        return m("table.table.table-condensed.table-justforlayout", [
             m("colgroup", [
                 m("col", {width:"5%"}),
                 m("col", {width:"25%"}),
@@ -103,101 +171,116 @@ window.TagEditorTable = {
                 ])
             ]),
             m("tbody", [
-                vnode.attrs.tags.map(function(tag, idx) {
+                vnode.attrs.tags.length > 0
+                ? vnode.attrs.tags.map(function(tag, idx) {
                     return m(TagEditorRow, {
                         key: idx,
-                        removeTag: function() { vnode.attrs.tags.splice(idx, 1) },
+                        removeTag: function() {
+                            vnode.attrs.tags.splice(idx, 1)
+                            vnode.attrs.dirty(true)
+                        },
                         editMode: vnode.attrs.editMode,
                         name: tag.name,
                         value: tag.value,
                         vocabulary: vnode.attrs.vocabulary
                     })
                 })
+                : m("tr", m("td[colspan=3]", m("center","loading tags...")))
             ]),
         ])
     }
 }
 
 window.TagEditorApp = {
+    appendTag: function(vnode, name, value) {
+        var tag = {name: m.stream(name), value: m.stream(value)}
+        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(m.redraw)
+    },
     oninit: function(vnode) {
-        vnode.state.saveLabel = m.stream(' Save ')
         vnode.state.sessionDB = new SessionDB()
         // Get vocabulary
-        vnode.state.vocabulary = m.stream({"strict":false, "types":{}})
+        vnode.state.vocabulary = m.stream({"strict":false, "tags":{}})
         m.request('/vocabulary.json').then(vnode.state.vocabulary)
         vnode.state.editMode = vnode.attrs.targetEditable
-        // Get tags
         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(), vnode.state.objPath, {
+            vnode.state.sessionDB.loadLocal(),
+            '/arvados/v1/'+vnode.attrs.targetController,
+            {
                 data: {
-                    select: JSON.stringify(['properties']) // FIXME: not working
+                    filters: JSON.stringify([['uuid', '=', vnode.attrs.targetUuid]]),
+                    select: JSON.stringify(['properties'])
                 },
             }).then(function(obj) {
-                console.log(obj)
-                Object.keys(obj.properties).forEach(function(k) {
-                    vnode.state.tags.push({
-                        name: m.stream(k),
-                        value: m.stream(obj.properties[k])
+                if (obj.items.length == 1) {
+                    o = obj.items[0]
+                    Object.keys(o.properties).forEach(function(k) {
+                        vnode.state.appendTag(vnode, k, o.properties[k])
                     })
-                })
-                vnode.state.dirty = m.stream(null)
-                vnode.state.tags.map(function(tag) {
-                  tag.name.map(m.redraw)
-                  tag.name.map(vnode.state.dirty)
-                  tag.value.map(vnode.state.dirty)
-                })
+                    // 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 [
+            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) {
+                        var tags = {}
+                        vnode.state.tags.forEach(function(t) {
+                            if (t.name() != '' && t.value() != '') {
+                                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.dirty() ? ' Save changes ' : ' Saved ')
+            ]),
             // Tags table
             m(TagEditorTable, {
                 editMode: vnode.state.editMode,
                 tags: vnode.state.tags,
-                vocabulary: 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.push({
-                                name: m.stream('new tag'),
-                                value: m.stream('new tag value')
-                            })
-                        }
-                    }, [
-                        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.saveLabel('Saving...')
-                            var tags = {}
-                            vnode.state.tags.forEach(function(t) {
-                                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.saveLabel(' Save ')
-                                console.log('ok!')
-                            })
-                        }
-                    }, vnode.state.saveLabel)
-                ])
-            ])
+                vocabulary: vnode.state.vocabulary,
+                dirty: vnode.state.dirty
+            })
         ]
     },
 }
\ No newline at end of file