Merge branch 'master' into 12479-wb-structured-vocabulary
authorLucas Di Pentima <ldipentima@veritasgenetics.com>
Wed, 20 Dec 2017 22:24:45 +0000 (19:24 -0300)
committerLucas Di Pentima <ldipentima@veritasgenetics.com>
Wed, 20 Dec 2017 22:24:45 +0000 (19:24 -0300)
Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>

apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/assets/javascripts/components/edit_tags.js [new file with mode: 0644]
apps/workbench/app/assets/javascripts/mithril_mount.js
apps/workbench/app/assets/javascripts/models/session_db.js
apps/workbench/app/assets/stylesheets/application.css.scss
apps/workbench/app/views/collections/_show_tags.html.erb
apps/workbench/npm_packages
apps/workbench/public/vocabulary.json [new file with mode: 0644]

index bba2f9dcc044b8b20b42e12cac8c62af5c4846f4..184c14b9ba239cfa9aa254c6c209f12823aa604d 100644 (file)
@@ -33,6 +33,9 @@
 //= require jquery.number.min
 //= require npm-dependencies
 //= require mithril/stream/stream
+//= require microplugin
+//= require sifter
+//= require selectize
 //= require_tree .
 
 Es6ObjectAssign.polyfill()
diff --git a/apps/workbench/app/assets/javascripts/components/edit_tags.js b/apps/workbench/app/assets/javascripts/components/edit_tags.js
new file mode 100644 (file)
index 0000000..f013b45
--- /dev/null
@@ -0,0 +1,208 @@
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// SPDX-License-Identifier: AGPL-3.0
+
+window.SelectOrAutocomplete = {
+    view: function(vnode) {
+        return m("input", {
+            style: {
+                width: '100%'
+            },
+            type: 'text',
+            value: vnode.attrs.value
+        }, vnode.attrs.value)
+    },
+    oncreate: function(vnode) {
+        $(vnode.dom).selectize({
+            labelField: 'value',
+            valueField: 'value',
+            searchField: 'value',
+            sortField: 'value',
+            maxItems: 1,
+            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) {
+                vnode.attrs.value(val)
+            }
+        })
+    }
+}
+
+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
+        }
+        valueOpts.push(vnode.attrs.value())
+
+        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 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),
+            // 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
+                })
+            ])
+            : 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%"}),
+            ]),
+            m("thead", [
+                m("tr", [
+                    m("th"),
+                    m("th", "Key"),
+                    m("th", "Value"),
+                ])
+            ]),
+            m("tbody", [
+                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)
+                            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","(no tags)")))
+            ]),
+        ])
+    }
+}
+
+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)
+        vnode.state.tags.push(tag)
+    },
+    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.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])
+                    })
+                    // Data synced with server, so dirty state should be false
+                    vnode.state.dirty(false)
+                }
+            }
+        )
+    },
+    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) {
+                            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,
+                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, 'new tag', 'new value')
+                    }
+                }, [
+                    m("i.glyphicon.glyphicon-plus"),
+                    " Add new tag "
+                ])
+            ])
+        ]
+    },
+}
\ No newline at end of file
index f4689b51d7ebfc265476dfa3e0b748ac33b62b94..7995ffea6ab8a69dd97030bef774de3599dfe5e6 100644 (file)
@@ -4,6 +4,7 @@
 
 $(document).on('ready arv:pane:loaded', function() {
     $('[data-mount-mithril]').each(function() {
-        m.mount(this, window[$(this).data('mount-mithril')])
+        var data = $(this).data()
+        m.mount(this, {view: function () {return m(window[data.mountMithril], data)}})
     })
 })
index ad9ad1878417370dfd75294e9bd9cecbe25880d1..54a61c23ecdad7b9dbab6045c7b432c55dcb73c6 100644 (file)
@@ -28,6 +28,17 @@ window.SessionDB = function() {
             })
             return sessions
         },
+        loadLocal: function() {
+            var sessions = db.loadActive()
+            var s = false
+            Object.values(sessions).forEach(function(session) {
+                if (session.isFromRails) {
+                    s = session
+                    return
+                }
+            })
+            return s
+        },
         save: function(k, v) {
             var sessions = db.loadAll()
             sessions[k] = v
index 32d80255d9fd831d2ea7f3e55ec163e53f154f06..b0c0100161b0eb7e26dddfe74bda32443985c472 100644 (file)
@@ -16,6 +16,8 @@
  *= require bootstrap
  *= require bootstrap3-editable/bootstrap-editable
  *= require morris
+ *= require selectize
+ *= require selectize.default
  *= require_tree .
  */
 
index afab5266e9c2e46dbe5c84425ba9f42b7f41f544..e3432d2d3f667e19ef7a934059b0915e5079844d 100644 (file)
@@ -2,51 +2,7 @@
 
 SPDX-License-Identifier: AGPL-3.0 %>
 
-<%
-  object = @object unless object
-%>
-
   <div class="collection-tags-container" style="padding-left:2em;padding-right:2em;">
-    <% if object.editable? %>
-      <p title="Edit tags" id="edit-collection-tags">
-        <a class="btn btn-primary edit-collection-tags">Edit</a>
-      </p>
-    <% end %>
-
-    <table class="table table-condensed table-fixedlayout collection-tags-table" border="1">
-      <colgroup>
-        <col width="5%" />
-        <col width="25%" />
-        <col width="70%" />
-      </colgroup>
-
-      <thead>
-        <tr>
-          <th></th>
-          <th>Key</th>
-          <th>Value</th>
-        </tr>
-      </thead>
-
-      <tbody class="collection-tag-rows">
-        <%= render partial: 'show_tag_rows', locals: {object: object} %>
-      </tbody>
-    </table>
-    <div>
-      <% if object.editable? %>
-        <div class="pull-left">
-          <a class="btn btn-primary btn-sm collection-tag-add hide"><i class="glyphicon glyphicon-plus"></i> Add new tag </a>
-        </div>
-        <div class="pull-right">
-          <%= link_to(save_tags_collection_path, {class: 'btn btn-sm btn-primary collection-tag-save hide', :remote => true, method: 'post', return_to: request.url}) do %>
-            Save
-          <% end %>
-          <%= link_to(tags_collection_path, {class: 'btn btn-sm btn-primary collection-tag-cancel hide', :remote => true, method: 'get', return_to: request.url}) do %>
-            Cancel
-          <% end %>
-        </div>
-
-        <div><div class="collection-tags-status"/></div></div>
-      <% end %>
-    </div>
+    <div data-mount-mithril="TagEditorApp" data-target-controller="<%= controller_name %>" data-target-uuid="<%= @object.uuid %>" data-target-editable="<%= @object.editable? %>"></div>
   </div>
\ No newline at end of file
index 2d57573dfd53dc3ceb5b90dccfe3bef363081ecc..1aa2575508ae4f7f2959fcffe1d75a30dd0e5100 100644 (file)
@@ -6,6 +6,10 @@
 
 # Browserify is required.
 npm 'browserify', require: false
+npm 'jquery'
+npm 'microplugin'
+npm 'sifter'
+npm 'selectize'
 
 npm 'mithril'
 npm 'es6-object-assign'
diff --git a/apps/workbench/public/vocabulary.json b/apps/workbench/public/vocabulary.json
new file mode 100644 (file)
index 0000000..c17cc18
--- /dev/null
@@ -0,0 +1,21 @@
+{
+    "strict": "false",
+    "types": {
+        "fruit": {
+            "type": "select",
+            "options": ["pineapple", "tomato", "orange", "banana"],
+            "overridable": "true"
+        },
+        "animal": {
+            "type": "select",
+            "options": ["human", "dog", "elefant", "eagle"]
+        },
+        "color": {
+            "type": "select",
+            "options": ["yellow", "red", "magenta", "green"]
+        },
+        "text tag": {
+            "type": "text"
+        }
+    }
+}
\ No newline at end of file