14738: Replaces double with single quotes for code style.
[arvados.git] / apps / workbench / app / assets / javascripts / components / edit_tags.js
index 040779629296517a5479435e5b10aebf009108d3..1fddb2651ef96a2cbec2e5dff1da030a0f33c3eb 100644 (file)
 //
 // 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.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)
+                        })
                     }
-                }, " Edit "),
+                }, vnode.state.dirty() ? ' Save changes ' : ' Saved ')
             ]),
             // 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
+}