Arvados-DCO-1.1-Signed-off-by: Lucas Di Pentima <ldipentima@veritasgenetics.com>
//= require jquery.number.min
//= require npm-dependencies
//= require mithril/stream/stream
+//= require microplugin
+//= require sifter
+//= require selectize
//= require_tree .
Es6ObjectAssign.polyfill()
--- /dev/null
+// Copyright (C) The Arvados Authors. All rights reserved.
+//
+// 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: {
+ width: '100%'
+ },
+ type: 'text',
+ value: vnode.attrs.value
+ }, vnode.attrs.value)
+ },
+ oncreate: function(vnode) {
+ 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,
+ items: [vnode.attrs.value()],
+ options: vnode.attrs.options.map(function(option) {
+ return {value: option}
+ }),
+ onChange: function(val) {
+ if (val != '') {
+ vnode.attrs.value(val)
+ }
+ },
+ onFocus: function() {
+ vnode.state.onFocus(vnode)
+ }
+ })
+ if (vnode.attrs.setFocus) {
+ vnode.state.selectized[0].selectize.focus()
+ }
+ }
+}
+
+window.TagEditorRow = {
+ view: function(vnode) {
+ 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())
+ }
+ }
+ 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(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(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
+ ])
+ ])
+ }
+}
+
+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%"}),
+ ]),
+ 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","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.sessionDB = new SessionDB()
+ // Get vocabulary
+ vnode.state.vocabulary = m.stream({"strict":false, "tags":{}})
+ 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)
+ // 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,
+ dirty: vnode.state.dirty
+ })
+ ]
+ },
+}
\ No newline at end of file
+++ /dev/null
-// Copyright (C) The Arvados Authors. All rights reserved.
-//
-// SPDX-License-Identifier: AGPL-3.0
-
-jQuery(function($){
- $(document).
- on('click', '.collection-tag-save, .collection-tag-cancel', function(event) {
- $('.edit-collection-tags').removeClass('disabled');
- $('#edit-collection-tags').attr("title", "Edit tags");
- $('.collection-tag-add').addClass('hide');
- $('.collection-tag-remove').addClass('hide');
- $('.collection-tag-save').addClass('hide');
- $('.collection-tag-cancel').addClass('hide');
- $('.collection-tag-field').prop("contenteditable", false);
- }).
- on('click', '.edit-collection-tags', function(event) {
- $('.edit-collection-tags').addClass('disabled');
- $('#edit-collection-tags').attr("title", "");
- $('.collection-tag-add').removeClass('hide');
- $('.collection-tag-remove').removeClass('hide');
- $('.collection-tag-save').removeClass('hide');
- $('.collection-tag-cancel').removeClass('hide');
- $('.collection-tag-field').prop("contenteditable", true);
- $('div').remove('.collection-tags-status-label');
- }).
- on('click', '.collection-tag-save', function(event) {
- var tag_data = {};
- var has_tags = false;
-
- var $tags = $(".collection-tags-table");
- $tags.find('tr').each(function (i, el) {
- var $tds = $(this).find('td');
- var $key = $tds.eq(1).text();
- if ($key && $key.trim().length > 0) {
- has_tags = true;
- tag_data[$key.trim()] = $tds.eq(2).text().trim();
- }
- });
-
- var to_send;
- if (has_tags == false) {
- to_send = {tag_data: "empty"}
- } else {
- to_send = {tag_data: tag_data}
- }
-
- $.ajax($(location).attr('pathname')+'/save_tags', {
- type: 'POST',
- data: to_send
- }).success(function(data, status, jqxhr) {
- $('.collection-tags-status').append('<div class="collection-tags-status-label alert alert-success"><p class="contain-align-left">Saved successfully.</p></div>');
- }).fail(function(jqxhr, status, error) {
- $('.collection-tags-status').append('<div class="collection-tags-status-label alert alert-danger"><p class="contain-align-left">We are sorry. There was an error saving tags. Please try again.</p></div>');
- });
- }).
- on('click', '.collection-tag-cancel', function(event) {
- $.ajax($(location).attr('pathname')+'/tags', {
- type: 'GET'
- });
- }).
- on('click', '.collection-tag-remove', function(event) {
- $(this).parents('tr').detach();
- }).
- on('click', '.collection-tag-add', function(event) {
- var $collection_tags = $(this).closest('.collection-tags-container');
- var $clone = $collection_tags.find('tr.hide').clone(true).removeClass('hide');
- $collection_tags.find('table').append($clone);
- }).
- on('keypress', '.collection-tag-field', function(event){
- return event.which != 13;
- });
-});
$(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)}})
})
})
})
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
// Guess workbench.{apihostport} is a Workbench... unless
// the host part of apihostport is an IPv4 or [IPv6]
// address.
- if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])'))
+ if (!session.baseURL.match('://(\\[|\\d+\\.\\d+\\.\\d+\\.\\d+[:/])')) {
var wbUrl = session.baseURL.replace('://', '://workbench.')
// Remove the trailing slash, if it's there.
return wbUrl.slice(-1) == '/' ? wbUrl.slice(0, -1) : wbUrl
+ }
return null
},
// Return a m.stream that will get fulfilled with the
*= require bootstrap
*= require bootstrap3-editable/bootstrap-editable
*= require morris
+ *= require selectize
+ *= require selectize.default
*= require_tree .
*/
end
end
- def tags
- render
- end
-
- def save_tags
- tags_param = params['tag_data']
- if tags_param
- if tags_param.is_a?(String) && tags_param == "empty"
- tags = {}
- else
- tags = tags_param
- end
- end
-
- if tags
- if @object.update_attributes properties: tags
- @saved_tags = true
- render
- else
- self.render_error status: 422
- end
- end
- end
-
protected
def find_usable_token(token_list)
+++ /dev/null
-<%# Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: AGPL-3.0 %>
-
-<%
- tags = object.properties
-%>
- <% if tags.andand.is_a?(Hash) %>
- <% tags.each do |k, v| %>
- <tr class="collection-tag-<%=k%>">
- <td>
- <% if object.editable? %>
- <i class="glyphicon glyphicon-remove collection-tag-remove hide" style="cursor: pointer;"></i>
- <% end %>
- </td>
- <td class="collection-tag-field collection-tag-field-key">
- <%= k %>
- </td>
- <td class="collection-tag-field collection-tag-field-value">
- <%= v %>
- </td>
- </tr>
- <% end %>
- <% end %>
-
- <% if @object.editable? %>
- <!-- A hidden row to add new tag -->
- <tr class="collection-tag-hidden hide">
- <td>
- <i class="glyphicon glyphicon-remove collection-tag-remove hide" style="cursor: pointer"></i>
- </td>
- <td class="collection-tag-field collection-tag-field-key"></td>
- <td class="collection-tag-field collection-tag-field-value"></td>
- </tr>
- <% end %>
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
+++ /dev/null
-<%# Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: AGPL-3.0 %>
-
-<% if @saved_tags %>
-$(".collection-tag-rows").html("<%= escape_javascript(render partial: 'show_tag_rows', locals: {object: @object}) %>");
-<% end %>
+++ /dev/null
-<%# Copyright (C) The Arvados Authors. All rights reserved.
-
-SPDX-License-Identifier: AGPL-3.0 %>
-
-$(".collection-tag-rows").html("<%= escape_javascript(render partial: 'show_tag_rows', locals: {object: @object}) %>");
# Browserify is required.
npm 'browserify', require: false
+npm 'jquery'
+npm 'microplugin'
+npm 'sifter'
+npm 'selectize'
npm 'mithril'
npm 'es6-object-assign'
--- /dev/null
+{
+ "strict": false,
+ "tags": {
+ "fruit": {
+ "values": ["pineapple", "tomato", "orange", "banana"],
+ "strict": true
+ },
+ "animal": {
+ "values": ["human", "dog", "elephant", "eagle"],
+ "strict": false
+ },
+ "color": {
+ "values": ["yellow", "red", "magenta", "green"],
+ "strict": false
+ },
+ "text tag": {}
+ }
+}
\ No newline at end of file
assert_response 422
assert_includes json_response['errors'], 'Duplicate file path'
end
-
- [
- [:active, true],
- [:spectator, false],
- ].each do |user, editable|
- test "tags tab #{editable ? 'shows' : 'does not show'} edit button to #{user}" do
- use_token user
-
- get :tags, {
- id: api_fixture('collections')['collection_with_tags_owned_by_active']['uuid'],
- format: :js,
- }, session_for(user)
-
- assert_response :success
-
- found = 0
- response.body.scan /<i[^>]+>/ do |remove_icon|
- remove_icon.scan(/\ collection-tag-remove(.*?)\"/).each do |i,|
- found += 1
- end
- end
-
- if editable
- assert_equal(3, found) # two from the tags + 1 from the hidden "add tag" row
- else
- assert_equal(0, found)
- end
- end
- end
-
- test "save_tags and verify that 'other' properties are retained" do
- use_token :active
-
- collection = api_fixture('collections')['collection_with_tags_owned_by_active']
-
- new_tags = {"new_tag1" => "new_tag1_value",
- "new_tag2" => "new_tag2_value"}
-
- post :save_tags, {
- id: collection['uuid'],
- tag_data: new_tags,
- format: :js,
- }, session_for(:active)
-
- assert_response :success
- assert_equal true, response.body.include?("new_tag1")
- assert_equal true, response.body.include?("new_tag1_value")
- assert_equal true, response.body.include?("new_tag2")
- assert_equal true, response.body.include?("new_tag2_value")
- assert_equal false, response.body.include?("existing tag 1")
- assert_equal false, response.body.include?("value for existing tag 1")
-
- updated_tags = Collection.find(collection['uuid']).properties
- assert_equal true, updated_tags.keys.include?(:'new_tag1')
- assert_equal new_tags['new_tag1'], updated_tags[:'new_tag1']
- assert_equal true, updated_tags.keys.include?(:'new_tag2')
- assert_equal new_tags['new_tag2'], updated_tags[:'new_tag2']
- assert_equal false, updated_tags.keys.include?(:'existing tag 1')
- assert_equal false, updated_tags.keys.include?(:'existing tag 2')
- end
end
first('.lock-collection-btn').click
accept_alert
end
-
- test "collection tags tab" do
- visit page_with_token('active', '/collections/zzzzz-4zz18-bv31uwvy3neko21')
-
- click_link 'Tags'
- wait_for_ajax
-
- # verify initial state
- assert_selector 'a', text: 'Edit'
- assert_no_selector 'a', text: 'Add new tag'
- assert_no_selector 'a', text: 'Save'
- assert_no_selector 'a', text: 'Cancel'
-
- # Verify controls in edit mode
- first('.edit-collection-tags').click
- assert_selector 'a.disabled', text: 'Edit'
- assert_selector 'a', text: 'Add new tag'
- assert_selector 'a', text: 'Save'
- assert_selector 'a', text: 'Cancel'
-
- # add two tags
- first('.glyphicon-plus').click
- first('.collection-tag-field-key').click
- first('.collection-tag-field-key').set('key 1')
- first('.collection-tag-field-value').click
- first('.collection-tag-field-value').set('value 1')
-
- first('.glyphicon-plus').click
- editable_key_fields = page.all('.collection-tag-field-key')
- editable_key_fields[1].click
- editable_key_fields[1].set('key 2')
- editable_val_fields = page.all('.collection-tag-field-value')
- editable_val_fields[1].click
- editable_val_fields[1].set('value 2')
-
- click_on 'Save'
- wait_for_ajax
-
- # added tags; verify
- assert_text 'key 1'
- assert_text 'value 1'
- assert_text 'key 2'
- assert_text 'value 2'
- assert_selector 'a', text: 'Edit'
- assert_no_selector 'a', text: 'Save'
-
- # remove first tag
- first('.edit-collection-tags').click
- assert_not_nil first('.glyphicon-remove')
- first('.glyphicon-remove').click
- click_on 'Save'
- wait_for_ajax
-
- assert_text 'key 2'
- assert_text 'value 2'
- assert_no_text 'key 1'
- assert_no_text 'value 1'
- assert_selector 'a', text: 'Edit'
-
- # Click on cancel and verify
- first('.edit-collection-tags').click
- first('.collection-tag-field-key').click
- first('.collection-tag-field-key').set('this key wont stick')
- first('.collection-tag-field-value').click
- first('.collection-tag-field-value').set('this value wont stick')
-
- click_on 'Cancel'
- wait_for_ajax
-
- assert_text 'key 2'
- assert_text 'value 2'
- assert_no_text 'this key wont stick'
- assert_no_text 'this value wont stick'
-
- # remove all tags
- first('.edit-collection-tags').click
- first('.glyphicon-remove').click
- click_on 'Save'
- wait_for_ajax
-
- assert_selector 'a', text: 'Edit'
- assert_no_text 'key 2'
- assert_no_text 'value 2'
- end
end