3235: Merge branch 'master' into 3235-top-nav-site-search
authorTom Clegg <tom@curoverse.com>
Mon, 28 Jul 2014 19:45:23 +0000 (15:45 -0400)
committerTom Clegg <tom@curoverse.com>
Mon, 28 Jul 2014 19:45:23 +0000 (15:45 -0400)
Conflicts:
apps/workbench/app/assets/javascripts/infinite_scroll.js
apps/workbench/app/views/application/_choose.html.erb
apps/workbench/app/views/layouts/application.html.erb

29 files changed:
apps/workbench/app/assets/javascripts/application.js
apps/workbench/app/assets/javascripts/filterable.js
apps/workbench/app/assets/javascripts/infinite_scroll.js
apps/workbench/app/assets/javascripts/select_modal.js
apps/workbench/app/assets/stylesheets/application.css.scss
apps/workbench/app/assets/stylesheets/select_modal.css.scss
apps/workbench/app/controllers/actions_controller.rb
apps/workbench/app/controllers/application_controller.rb
apps/workbench/app/controllers/collections_controller.rb
apps/workbench/app/controllers/search_controller.rb [new file with mode: 0644]
apps/workbench/app/helpers/application_helper.rb
apps/workbench/app/models/group.rb
apps/workbench/app/views/application/_choose.html.erb
apps/workbench/app/views/application/_choose.js.erb
apps/workbench/app/views/application/_projects_tree_menu.html.erb
apps/workbench/app/views/collections/_choose_rows.html.erb
apps/workbench/app/views/groups/_choose_rows.html.erb
apps/workbench/app/views/layouts/application.html.erb
apps/workbench/app/views/pipeline_templates/_choose_rows.html.erb
apps/workbench/app/views/search/_choose_rows.html.erb [new file with mode: 0644]
apps/workbench/app/views/users/_choose_rows.html.erb
apps/workbench/config/routes.rb
apps/workbench/test/controllers/search_controller_test.rb [new file with mode: 0644]
apps/workbench/test/helpers/search_helper_test.rb [new file with mode: 0644]
apps/workbench/test/integration/smoke_test.rb
services/api/app/controllers/application_controller.rb
services/api/app/controllers/arvados/v1/groups_controller.rb
services/api/config/routes.rb
services/api/test/functional/arvados/v1/groups_controller_test.rb

index d21c4b5625447617b9f4ceaea33f8762e8f69f26..cbca4c0fc3973ac687ffbd38dc3524aa1fa801cd 100644 (file)
@@ -135,11 +135,13 @@ jQuery(function($){
         on('ajax:complete ready', function() {
             // See http://getbootstrap.com/javascript/#buttons
             $('.btn').button();
-        });
-
-    $(document).
+        }).
         on('ready ajax:complete', function() {
             $('[data-toggle~=tooltip]').tooltip({container:'body'});
+        }).
+        on('ready ajax:complete', function() {
+            // This makes the dialog close on Esc key, obviously.
+            $('.modal').attr('tabindex', '-1')
         });
 
     HeaderRowFixer = function(selector) {
index 76c5ac3ca1297d23ee8bc867444c231ef177a993..098c7ec5eb47fe8918670360e5ba17b38ba6476b 100644 (file)
@@ -6,6 +6,7 @@ $(document).
             data('q', q).
             trigger('refresh');
     }).on('refresh', '.filterable-container', function() {
+        var $container = $(this);
         var q = $(this).data('q');
         var filters = $(this).data('filters');
         $('.filterable', this).hide().filter(function() {
@@ -26,6 +27,16 @@ $(document).
             }
             return pass;
         }).show();
+
+        // Show/hide each section heading depending on whether any
+        // content rows are visible in that section.
+        $('.row[data-section-heading]', this).each(function(){
+            $(this).toggle($('.row.filterable[data-section-name="' +
+                             $(this).attr('data-section-name') +
+                             '"]:visible').length > 0);
+        });
+
+        // Load more content if the last result is showing.
         $('.infinite-scroller').add(window).trigger('scroll');
     }).on('change', 'select.filterable-control', function() {
         var val = $(this).val();
index 1f9997efa05fc002f8a9907965d73e9dff2b1a46..73b8fd66af36ee72a019cf1ac144509f3099b3d9 100644 (file)
@@ -4,6 +4,7 @@ function maybe_load_more_content() {
     var src;                    // url for retrieving content
     var scrollHeight;
     var spinner, colspan;
+    var serial = Date.now();
     scrollHeight = scroller.scrollHeight || $('body')[0].scrollHeight;
     var num_scrollers = $(window).data("arv-num-scrollers");
     if ($(scroller).scrollTop() + $(scroller).height()
@@ -12,9 +13,16 @@ function maybe_load_more_content() {
     {
       for (var i = 0; i < num_scrollers; i++) {
         $container = $($(this).data('infinite-container'+i));
-        src = $container.attr('data-infinite-content-href');
+        if (!$(container).attr('data-infinite-content-href0')) {
+            // Remember the first page source url, so we can refresh
+            // from page 1 later.
+            $(container).attr('data-infinite-content-href0',
+                              $(container).attr('data-infinite-content-href'));
+        }
+        src = $(container).attr('data-infinite-content-href');
         if (!src || !$container.is(':visible'))
-          continue;
+            // Finished
+            return;
 
         // Don't start another request until this one finishes
         $container.attr('data-infinite-content-href', null);
@@ -30,16 +38,20 @@ function maybe_load_more_content() {
                        spinner +
                        '</td></tr>');
         }
-        $container.append(spinner);
+        $(container).find(".spinner").detach();
+        $(container).append(spinner);
+        $(container).attr('data-infinite-serial', serial);
         $.ajax(src,
                {dataType: 'json',
                 type: 'GET',
-                data: {},
-                context: {container: $container, src: src}}).
-            always(function() {
-                $(this.container).find(".spinner").detach();
-            }).
+                data: ($(container).data('infinite-content-params') || {}),
+                context: {container: container, src: src, serial: serial}}).
             fail(function(jqxhr, status, error) {
+                var $faildiv;
+                if ($(this.container).attr('data-infinite-serial') != this.serial) {
+                    // A newer request is already in progress.
+                    return;
+                }
                 if (jqxhr.readyState == 0 || jqxhr.status == 0) {
                     message = "Cancelled."
                 } else if (jqxhr.responseJSON && jqxhr.responseJSON.errors) {
@@ -47,11 +59,20 @@ function maybe_load_more_content() {
                 } else {
                     message = "Request failed.";
                 }
-                // TODO: report this to the user.
+                // TODO: report the message to the user.
                 console.log(message);
-                $(this.container).attr('data-infinite-content-href', this.src);
+                $faildiv = $('<div />').
+                    attr('data-infinite-content-href', this.src).
+                    addClass('infinite-retry').
+                    append('<span class="fa fa-warning" /> Oops, request failed. <button class="btn btn-xs btn-primary">Retry</button>');
+                $(this.container).find('div.spinner').replaceWith($faildiv);
             }).
             done(function(data, status, jqxhr) {
+                if ($(this.container).attr('data-infinite-serial') != this.serial) {
+                    // A newer request is already in progress.
+                    return;
+                }
+                $(this.container).find(".spinner").detach();
                 $(this.container).append(data.content);
                 $(this.container).attr('data-infinite-content-href', data.next_page_href);
             });
@@ -61,6 +82,26 @@ function maybe_load_more_content() {
 }
 
 $(document).
+    on('click', 'div.infinite-retry button', function() {
+        var $retry_div = $(this).closest('.infinite-retry');
+        var $scroller = $(this).closest('.infinite-scroller')
+        $scroller.attr('data-infinite-content-href',
+                       $retry_div.attr('data-infinite-content-href'));
+        $retry_div.replaceWith('<div class="spinner spinner-32px spinner-h-center" />');
+        $scroller.trigger('scroll');
+    }).
+    on('refresh-content', '[data-infinite-scroller]', function() {
+        // Clear all rows, reset source href to initial state, and
+        // (if the container is visible) start loading content.
+        var first_page_href = $(this).attr('data-infinite-content-href0');
+        if (!first_page_href)
+            first_page_href = $(this).attr('data-infinite-content-href');
+        $(this).
+            html('').
+            attr('data-infinite-content-href', first_page_href);
+        $('.infinite-scroller').
+            trigger('scroll');
+    }).
     on('ready ajax:complete', function() {
         var num_scrollers = 0;
         $('[data-infinite-scroller]').each(function() {
index cd23556a4accc83007172bd2d3e24cfea261d1cb..7c723236a0ce54ba72e27108a5fdad69eabeb441 100644 (file)
@@ -1,14 +1,14 @@
 $(document).on('click', '.selectable', function() {
     var any;
     var $this = $(this);
-    if (!$this.hasClass('multiple')) {
-        $this.closest('.selectable-container').
+    var $container = $(this).closest('.selectable-container');
+    if (!$container.hasClass('multiple')) {
+        $container.
             find('.selectable').
             removeClass('active');
     }
     $this.toggleClass('active');
-    any = ($this.
-           closest('.selectable-container').
+    any = ($container.
            find('.selectable.active').length > 0)
     $this.
         closest('.modal').
@@ -16,14 +16,19 @@ $(document).on('click', '.selectable', function() {
         prop('disabled', !any);
 
     if ($this.hasClass('active')) {
+        var no_preview_available = '<div class="spinner-h-center spinner-v-center"><center>(No preview available)</center></div>';
+        if (!$this.attr('data-preview-href')) {
+            $(".modal-dialog-preview-pane").html(no_preview_available);
+            return;
+        }
         $(".modal-dialog-preview-pane").html('<div class="spinner spinner-32px spinner-h-center spinner-v-center"></div>');
         $.ajax($this.attr('data-preview-href'),
                {dataType: "html"}).
-           done(function(data, status, jqxhr) {
+            done(function(data, status, jqxhr) {
                 $(".modal-dialog-preview-pane").html(data);
             }).
             fail(function(data, status, jqxhr) {
-                $(".modal-dialog-preview-pane").text('Preview load failed.');
+                $(".modal-dialog-preview-pane").html(no_preview_available);
             });
     }
 
@@ -65,6 +70,17 @@ $(document).on('click', '.selectable', function() {
             $(document).trigger(event_name!=null ? event_name : 'page-refresh',
                                 [data, status, jqxhr, this.action_data]);
         });
+}).on('click', '.chooser-show-project', function() {
+    var params = {};
+    $(this).attr('href', '#');  // Skip normal click handler
+    if ($(this).attr('data-project-uuid')) {
+        params = {'filters[]': JSON.stringify(['owner_uuid',
+                                               '=',
+                                               $(this).attr('data-project-uuid')])};
+    }
+    $($(this).closest('[data-filterable-target]').attr('data-filterable-target')).
+        data('infinite-content-params', params).
+        trigger('refresh-content');
 });
 $(document).on('page-refresh', function(event, data, status, jqxhr, action_data) {
     window.location.reload();
index 75d58ed5171184bf7d855267ed99ac4d353f7758..c986f034bcc6a0e19f8525659201a3fe8f6cb74c 100644 (file)
@@ -146,6 +146,10 @@ nav.navbar-fixed-top .navbar-nav.navbar-right > li > a:hover {
     margin-bottom: -15px;
 }
 
+.infinite-scroller .fa-warning {
+    color: #800;
+}
+
 .inline-progress-container div.progress {
     margin-bottom: 0;
 }
index 09017697b646298a2a71d34b5c16c60db662a5bb..4295f30ecef75308bd2feea0e452205b5358cc12 100644 (file)
@@ -14,3 +14,6 @@
     background: #428bca;
     color: #fff;
 }
+.selectable-container > .row.class-separator {
+    background: #ddd;
+}
index 9a76e9aed412522ffa14166e0b9aadbe1f8e8acf..9f2cfb09f3da31ecb6953b5e5a3e915b212d757f 100644 (file)
@@ -10,6 +10,17 @@ class ActionsController < ApplicationController
     ArvadosBase::resource_class_for_uuid(params[:uuid])
   end
 
+  def show
+    @object = model_class.find(params[:uuid])
+    if @object.is_a? Link and
+        @object.link_class == 'name' and
+        ArvadosBase::resource_class_for_uuid(@object.head_uuid) == Collection
+      redirect_to collection_path(id: @object.uuid)
+    else
+      redirect_to @object
+    end
+  end
+
   def post
     params.keys.collect(&:to_sym).each do |param|
       if @@exposed_actions[param]
index f739ee1046bb569f6e55bd4aef72cbd786d7fc26..e84c3c0fc25a7a0a4207004de5e5d4f35b58d171 100644 (file)
@@ -14,6 +14,7 @@ class ApplicationController < ActionController::Base
   around_filter :require_thread_api_token, except: ERROR_ACTIONS
   before_filter :check_user_agreements, except: ERROR_ACTIONS
   before_filter :check_user_notifications, except: ERROR_ACTIONS
+  before_filter :load_filters_and_paging_params, except: ERROR_ACTIONS
   before_filter :find_object_by_uuid, except: [:index, :choose] + ERROR_ACTIONS
   theme :select_theme
 
@@ -86,7 +87,7 @@ class ApplicationController < ActionController::Base
     end
   end
 
-  def find_objects_for_index
+  def load_filters_and_paging_params
     @limit ||= 200
     if params[:limit]
       @limit = params[:limit].to_i
@@ -102,10 +103,22 @@ class ApplicationController < ActionController::Base
       filters = params[:filters]
       if filters.is_a? String
         filters = Oj.load filters
+      elsif filters.is_a? Array
+        filters = filters.collect do |filter|
+          if filter.is_a? String
+            # Accept filters[]=["foo","=","bar"]
+            Oj.load filter
+          else
+            # Accept filters=[["foo","=","bar"]]
+            filter
+          end
+        end
       end
       @filters += filters
     end
+  end
 
+  def find_objects_for_index
     @objects ||= model_class
     @objects = @objects.filter(@filters).limit(@limit).offset(@offset)
   end
@@ -148,6 +161,13 @@ class ApplicationController < ActionController::Base
     end
   end
 
+  helper_method :next_page_href
+  def next_page_href with_params={}
+    if next_page_offset
+      url_for with_params.merge(offset: next_page_offset)
+    end
+  end
+
   def show
     if !@object
       return render_not_found("object not found")
@@ -171,26 +191,14 @@ class ApplicationController < ActionController::Base
 
   def choose
     params[:limit] ||= 40
-    if !@objects
-      if params[:project_uuid] and !params[:project_uuid].empty?
-        # We want the chooser to show objects of the controllers's model_class
-        # type within a specific project specified by project_uuid, so fetch the
-        # project and request the contents of the project filtered on the
-        # controllers's model_class kind.
-        @objects = Group.find(params[:project_uuid]).contents({:filters => [['uuid', 'is_a', "arvados\##{ArvadosApiClient.class_kind(model_class)}"]]})
-      end
-      find_objects_for_index if !@objects
-    end
+    find_objects_for_index if !@objects
     respond_to do |f|
       if params[:partial]
         f.json {
           render json: {
             content: render_to_string(partial: "choose_rows.html",
-                                      formats: [:html],
-                                      locals: {
-                                        multiple: params[:multiple]
-                                      }),
-            next_page_href: @next_page_href
+                                      formats: [:html]),
+            next_page_href: next_page_href(partial: params[:partial])
           }
         }
       end
index 5a7a52207bcebdc946eb3289c618d97b2cddd78c..fd78ddf84062e828ac72489d99398c05dec6c1e9 100644 (file)
@@ -45,18 +45,12 @@ class CollectionsController < ApplicationController
   def choose
     params[:limit] ||= 40
 
-    filter = [['link_class','=','name'],
-              ['head_uuid','is_a','arvados#collection']]
-
-    if params[:project_uuid] and !params[:project_uuid].empty?
-      filter << ['tail_uuid', '=', params[:project_uuid]]
-    end
-
-    @objects = Link.filter(filter)
+    @filters += [['link_class','=','name'],
+                 ['head_uuid','is_a','arvados#collection']]
 
+    @objects = Link
     find_objects_for_index
-    @next_page_href = (next_page_offset and
-                       url_for(offset: next_page_offset, partial: true))
+
     @name_links = @objects
 
     @objects = Collection.
diff --git a/apps/workbench/app/controllers/search_controller.rb b/apps/workbench/app/controllers/search_controller.rb
new file mode 100644 (file)
index 0000000..2b0ad7e
--- /dev/null
@@ -0,0 +1,10 @@
+class SearchController < ApplicationController
+  def find_objects_for_index
+    @objects = Group.contents(limit: @limit, offset: @offset, filters: @filters)
+    super
+  end
+
+  def next_page_href with_params={}
+    super with_params.merge(last_object_class: @objects.last.class.to_s)
+  end
+end
index c3856c2f299bfb0747d7cc81d5394338e9a67547..1b186c5420fc85739559fe0fabbf8bb20204bf93 100644 (file)
@@ -428,4 +428,13 @@ module ApplicationHelper
       RESOURCE_CLASS_ICONS.fetch(class_name, default)
     end
   end
+
+  def chooser_preview_url_for object
+    case object.class.to_s
+    when 'Collection'
+      polymorphic_path(object, tab_pane: 'chooser_preview')
+    else
+      nil
+    end
+  end
 end
index 9e627bf66ea012871ee8a536799d79ae40de3e9a..30488c3383b812bb7ffc32dbec524bdb88c1c2cf 100644 (file)
@@ -3,6 +3,15 @@ class Group < ArvadosBase
     true
   end
 
+  def self.contents params={}
+    res = arvados_api_client.api self, "/contents", {
+      _method: 'GET'
+    }.merge(params)
+    ret = ArvadosResourceList.new
+    ret.results = arvados_api_client.unpack_api_response(res)
+    ret
+  end
+
   def contents params={}
     res = arvados_api_client.api self.class, "/#{self.uuid}/contents", {
       _method: 'GET'
index 3c1aaadf7d5746d34b612a208195a9ec583da726..7af37e0ae92415dc62f1c5d9b8921b55dbd4fee0 100644 (file)
@@ -7,50 +7,45 @@
       </div>
 
       <div class="modal-body">
-        <nav class="navbar navbar-default breadcrumbs" role="navigation">
-          <ul class="nav navbar-nav navbar-left">
-            <li class="dropdown">
-            <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="projects-menu">
-              Projects
-              <span class="caret"></span>
-            </a>
-            <ul class="dropdown-menu" role="menu">
-            <%= render partial: "projects_tree_menu", locals: {
-                  :project_link_to => Proc.new do |pnode, &block|
-                    link_to "#", {"class" => "chooser-show-project", "data-project-uuid" => pnode[:object].uuid }, &block
-                   end,
-                  :top_button => Proc.new do %>
-                    <% link_to "#", {"class" => "chooser-show-project btn btn-xs btn-default pull-right" } do %>
-                      All <%= controller.model_class.class_for_display.pluralize.downcase %>
-                    <% end %>
+        <div class="input-group">
+          <input type="text" class="form-control filterable-control focus-on-display" placeholder="Search" data-filterable-target=".modal.arv-choose .selectable-container"/>
+          <% if params[:by_project] != false %>
+            <div class="input-group-btn" data-filterable-target=".modal.arv-choose .selectable-container">
+              <button type="button" class="btn btn-default dropdown-toggle" data-toggle="dropdown">
+                Filter by project <span class="caret"></span>
+              </button>
+              <ul class="dropdown-menu" role="menu">
+                <li>
+                  <%= link_to '#', class: 'chooser-show-project' do %>
+                    Remove filter (search all projects)
                   <% end %>
-            <% } %>
-            </ul>
-            </li>
-            <li class="nav-separator">
-              <i class="fa fa-lg fa-angle-double-right"></i>
-            </li>
-            <li><p class="navbar-text" id="chooser-breadcrumb">All <%= controller.model_class.class_for_display.pluralize.downcase %></span></li>
-          </ul>
-          <div class="navbar-form navbar-right">
-            <input type="text" class="form-control filterable-control" placeholder="Search" data-filterable-target=".modal.arv-choose .selectable-container"/>
-          </div>
-        </nav>
+                </li>
+                <li class="divider" />
+                <%= render partial: "projects_tree_menu", locals: {
+                      :project_link_to => Proc.new do |pnode, &block|
+                        link_to "#", {"class" => "chooser-show-project", "data-project-uuid" => pnode[:object].uuid }, &block
+                       end,
+                      :top_button => nil
+                    } %>
+              </ul>
+            </div>
+          <% end %>
+        </div>
+        <div style="height: 1em" />
 
         <% preview_pane = (params[:preview_pane] != "false")
            pane_col_class = preview_pane ? "col-sm-6" : "" %>
         <div class="row" style="height: 20em">
-          <div class="<%= pane_col_class %> container-fluid arv-filterable-list selectable-container"
-               style="height: 100%; overflow-y: scroll"
-               data-infinite-scroller="#choose-scroll"
-               id="choose-scroll"
-               data-infinite-content-href="<%= @next_page_href %>">
-            <%= render partial: 'choose_rows', locals: {multiple: multiple} %>
+         <div class="col-sm-6 container arv-filterable-list selectable-container <%= 'multiple' if multiple %>"
+              style="height: 100%; overflow-y: scroll"
+              data-infinite-scroller="#choose-scroll"
+              id="choose-scroll"
+              data-infinite-content-href="<%= next_page_href partial: true %>"
+              data-infinite-content-href0="<%= url_for partial: true %>">
+           <%= render partial: 'choose_rows' %>
+         </div>
+          <div class="col-sm-6 modal-dialog-preview-pane" style="height: 100%; overflow-y: scroll">
           </div>
-          <% if preview_pane %>
-          <div class="<%= pane_col_class %> modal-dialog-preview-pane" style="height: 100%; overflow-y: scroll">
-          </div>
-          <% end %>
         </div>
 
         <div class="modal-footer">
index b033c9bf2fedf170c765b178bb510494707888ca..2d189753ff2c96d9a0770f847d448055f9981f6d 100644 (file)
@@ -5,24 +5,4 @@ $('body > .modal-container .modal .modal-footer .btn-primary').
     attr('data-action-href', '<%= j params[:action_href] %>').
     attr('data-method', '<%= j params[:action_method] %>').
     data('action-data', <%= raw params[:action_data] %>);
-$(".chooser-show-project").on("click", function() {
-  $("#choose-scroll").html("<div class=\"spinner spinner-32px spinner-h-center\"></div>");
-  $(".modal-dialog-preview-pane").html('');
-  var t = $(this);
-  var d = {
-      partial: true,
-      multiple: <%= multiple || "false" %>
-    };
-  if (t.attr("data-project-uuid") != null) {
-    d.project_uuid = t.attr("data-project-uuid");
-  }
-  $.ajax('<%=j url_for %>', {
-    dataType: "json",
-    type: "GET",
-    data: d
-  }).done(function(data, status, jqxhr) {
-    $("#chooser-breadcrumb").text(t.text());
-    $("#choose-scroll").html(data.content);
-    $("#choose-scroll").prop("data-infinite-content-href", "next_page_href");
-  });
-});
+$('body > .modal-container .modal .focus-on-display').focus();
index 876b0be65c8120e9d64a16b5d2cf03f73017c944..3a1aa9e81c4a71bedbcd58f5058502e099018503 100644 (file)
@@ -1,5 +1,5 @@
               <li role="presentation" class="dropdown-header">
-                <%= top_button.call %>
+                <%= top_button.andand.call %>
                 My projects
               </li>
               <% my_project_tree.each do |pnode| %>
index d87f56f9cd4136c40009c71e67113271b6fde2b5..5a019f82258cff6df2a09e25ee51cb724d268dc3 100644 (file)
@@ -1,7 +1,7 @@
 <% @name_links.each do |name_link| %>
   <% if (object = get_object(name_link.head_uuid)) %>
-    <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= name_link.uuid %>"
-         data-preview-href="<%= url_for object %>?tab_pane=chooser_preview"
+    <div class="row filterable selectable" data-object-uuid="<%= name_link.uuid %>"
+         data-preview-href="<%= chooser_preview_url_for object %>"
          style="margin-left: 1em; border-bottom-style: solid; border-bottom-width: 1px; border-bottom-color: #DDDDDD">
         <i class="fa fa-fw fa-archive"></i>
         <%= name_link.name %>
index 772ef197d03410a561106edb09e852190d6840c4..fca0415e292e3ce06f462f371c8ba2a7734d9866 100644 (file)
@@ -1,6 +1,6 @@
 <% icon_class = fa_icon_class_for_class(Group) %>
 <% @objects.each do |object| %>
-  <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= object.uuid %>">
+  <div class="row filterable selectable" data-object-uuid="<%= object.uuid %>">
     <div class="col-sm-12" style="overflow-x:hidden">
       <i class="fa fa-fw <%= icon_class %>"></i>
       <%= object.name %>
index d0b27c375823cfdf3d7142c1124075a279f3e523..bd6a6aae4097cad0dadbe6004d2488c1e993a7b5 100644 (file)
           </li>
           -->
 
+          <li>
+            <%= link_to(url_for(
+                       action: 'choose',
+                       controller: 'search',
+                       title: 'Search',
+                       action_name: 'Show',
+                       action_href: url_for(controller: :actions, action: :show),
+                       action_method: 'get',
+                       action_data: {selection_param: 'uuid', success: 'redirect-to-created-object'}.to_json),
+                      { class: "", remote: true, method: 'get' }) do %>
+              <i class="fa fa-fw fa-search"></i> Search
+            <% end %>
+          </li>
+
           <li class="dropdown notification-menu">
             <a href="#" class="dropdown-toggle" data-toggle="dropdown" id="notifications-menu">
               <span class="badge badge-alert notification-count"><%= @notification_count %></span>
index 9aebd695bf681cb25214bbeab566a45444d9d5f1..9b96b47d19a5193ee218d01ced11702716a13b54 100644 (file)
@@ -1,5 +1,5 @@
 <% @objects.each do |object| %>
-  <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= object.uuid %>" data-preview-href="<%= url_for object %>?tab_pane=chooser_preview">
+  <div class="row filterable selectable" data-object-uuid="<%= object.uuid %>" data-preview-href="<%= url_for object %>?tab_pane=chooser_preview">
     <div class="col-sm-12" style="overflow-x:hidden">
       <i class="fa fa-fw fa-gear"></i>
       <%= object.name %>
diff --git a/apps/workbench/app/views/search/_choose_rows.html.erb b/apps/workbench/app/views/search/_choose_rows.html.erb
new file mode 100644 (file)
index 0000000..81d1f68
--- /dev/null
@@ -0,0 +1,22 @@
+<% current_class = params[:last_object_class] %>
+<% @objects.each do |object| %>
+  <% icon_class = fa_icon_class_for_class(object.class) %>
+  <% if object.class.to_s != current_class %>
+    <% current_class = object.class.to_s %>
+    <div class="row class-separator" data-section-heading="true" data-section-name="<%= object.class.to_s %>">
+      <div class="col-sm-12">
+        <%= object.class_for_display.pluralize.downcase %>
+      </div>
+    </div>
+  <% end %>
+  <div class="row filterable selectable" data-section-name="<%= object.class.to_s %>" data-object-uuid="<%= object.uuid %>" data-preview-href="<%= chooser_preview_url_for object %>">
+    <div class="col-sm-12" style="overflow-x:hidden; white-space: nowrap">
+      <i class="fa fa-fw <%= icon_class %>"></i>
+      <% if object.respond_to?(:name) and object.name and object.name.length > 0 %>
+        <%= object.name %>
+      <% else %>
+        unnamed - <span class="arvados-uuid"><%= object.uuid %></span>
+      <% end %>
+    </div>
+  </div>
+<% end %>
index a893e5e6d1e35ec7fc1a65f9b830d8bcf185e6f3..28f918719352473138e041c58f4e5a2bf42da0bf 100644 (file)
@@ -1,6 +1,6 @@
 <% icon_class = fa_icon_class_for_class(User) %>
 <% @objects.each do |object| %>
-  <div class="row filterable selectable <%= 'multiple' if multiple %>" data-object-uuid="<%= object.uuid %>">
+  <div class="row filterable selectable" data-object-uuid="<%= object.uuid %>">
     <div class="col-sm-12" style="overflow-x:hidden">
       <i class="fa fa-fw <%= icon_class %>"></i>
       <%= object.full_name %>
index 55e2fdae364465ef07958d728e9b992b26fffa5a..fe6200bda8edb5e1ace1dab3551dcf13f04164f9 100644 (file)
@@ -66,8 +66,12 @@ ArvadosWorkbench::Application.routes.draw do
     get 'choose', on: :collection
     post 'share_with', on: :member
   end
+  resources :search do
+    get 'choose', :on => :collection
+  end
 
   post 'actions' => 'actions#post'
+  get 'actions' => 'actions#show'
   get 'websockets' => 'websocket#index'
 
   root :to => 'projects#index'
diff --git a/apps/workbench/test/controllers/search_controller_test.rb b/apps/workbench/test/controllers/search_controller_test.rb
new file mode 100644 (file)
index 0000000..bfbf22d
--- /dev/null
@@ -0,0 +1,7 @@
+require 'test_helper'
+
+class SearchControllerTest < ActionController::TestCase
+  # test "the truth" do
+  #   assert true
+  # end
+end
diff --git a/apps/workbench/test/helpers/search_helper_test.rb b/apps/workbench/test/helpers/search_helper_test.rb
new file mode 100644 (file)
index 0000000..3034163
--- /dev/null
@@ -0,0 +1,4 @@
+require 'test_helper'
+
+class SearchHelperTest < ActionView::TestCase
+end
index efaea00b01c42ff4a06734e07be8505cf28125f4..1e3337044738cf3bca873d1545c96c850f636df0 100644 (file)
@@ -16,6 +16,9 @@ class SmokeTest < ActionDispatch::IntegrationTest
     all(find_spec + ' a').collect { |tag|
       if tag[:href].nil? or tag[:href].empty? or (tag.text !~ text_regexp)
         nil
+      elsif tag[:'data-remote']
+        # these don't necessarily work with format=html
+        nil
       else
         url = URI(tag[:href])
         url.host.nil? ? url.path : nil
index 8464a4ab5e05a515a557c5bdd9407724374642ef..58d14fd559368f896216bf123331168a9557aca0 100644 (file)
@@ -35,7 +35,7 @@ class ApplicationController < ActionController::Base
                 except: [:index, :create] + ERROR_ACTIONS)
   before_filter :load_limit_offset_order_params, only: [:index, :contents]
   before_filter :load_where_param, only: [:index, :contents]
-  before_filter :load_filters_param, only: [:index, :contents]
+  before_filter :load_filters_param, only: [:index, :contents, :choose]
   before_filter :find_objects_for_index, :only => :index
   before_filter :reload_object_before_update, :only => :update
   before_filter(:render_404_if_no_object,
index da82e81ef81374e39994b08a27518e3f60de84e6..98acbef65ca97b05eb61366b082669381b977af7 100644 (file)
@@ -9,9 +9,44 @@ class Arvados::V1::GroupsController < ApplicationController
             })
   end
 
+  def render_404_if_no_object
+    if params[:action] == 'contents' and !params[:uuid]
+      # OK!
+      @object = nil
+    else
+      super
+    end
+  end
+
   def contents
+    # Set @objects:
+    load_searchable_objects(owner_uuid: @object.andand.uuid, include_linked: params[:include_linked])
+
+    sql = 'link_class=? and head_uuid in (?)'
+    sql_params = ['name', @objects.collect(&:uuid)]
+    if @object
+      sql += ' and tail_uuid=?'
+      sql_params << @object.uuid
+    end
+    @links = Link.where sql, *sql_params
+    @object_list = {
+      :kind  => "arvados#objectList",
+      :etag => "",
+      :self_link => "",
+      :links => @links.as_api_response(nil),
+      :offset => @offset,
+      :limit => @limit,
+      :items_available => @items_available,
+      :items => @objects.as_api_response(nil)
+    }
+    render json: @object_list
+  end
+
+  protected
+
+  def load_searchable_objects opts
     all_objects = []
-    all_available = 0
+    @items_available = 0
 
     # Trick apply_where_limit_order_params into applying suitable
     # per-table values. *_all are the real ones we'll apply to the
@@ -25,39 +60,34 @@ class Arvados::V1::GroupsController < ApplicationController
      Collection,
      Human, Specimen, Trait].each do |klass|
       @objects = klass.readable_by(*@read_users)
-      cond_sql = "#{klass.table_name}.owner_uuid = ?"
-      cond_params = [@object.uuid]
-      if params[:include_linked]
-        cond_sql += " OR #{klass.table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class=#{klass.sanitize 'name'} AND links.tail_uuid=#{klass.sanitize @object.uuid})"
+      conds = []
+      cond_params = []
+      if opts[:owner_uuid]
+        conds << "#{klass.table_name}.owner_uuid = ?"
+        cond_params << opts[:owner_uuid]
+      end
+      if opts[:include_linked]
+        conds << "#{klass.table_name}.uuid IN (SELECT head_uuid FROM links WHERE link_class=#{klass.sanitize 'name'} AND links.tail_uuid=#{klass.sanitize @object.uuid})"
+      end
+      if conds.any?
+        cond_sql = '(' + conds.join(') OR (') + ')'
+        @objects = @objects.where(cond_sql, *cond_params)
       end
-      @objects = @objects.where(cond_sql, *cond_params).order("#{klass.table_name}.uuid")
+      @objects = @objects.order("#{klass.table_name}.uuid")
       @limit = limit_all - all_objects.count
       apply_where_limit_order_params
-      items_available = @objects.
+      klass_items_available = @objects.
         except(:limit).except(:offset).
         count(:id, distinct: true)
-      all_available += items_available
-      @offset = [@offset - items_available, 0].max
+      @items_available += klass_items_available
+      @offset = [@offset - klass_items_available, 0].max
 
       all_objects += @objects.to_a
     end
-    @objects = all_objects || []
-    @links = Link.where('link_class=? and tail_uuid=?'\
-                        ' and head_uuid in (?)',
-                        'name',
-                        @object.uuid,
-                        @objects.collect(&:uuid))
-    @object_list = {
-      :kind  => "arvados#objectList",
-      :etag => "",
-      :self_link => "",
-      :links => @links.as_api_response(nil),
-      :offset => offset_all,
-      :limit => limit_all,
-      :items_available => all_available,
-      :items => @objects.as_api_response(nil)
-    }
-    render json: @object_list
+
+    @objects = all_objects
+    @limit = limit_all
+    @offset = offset_all
   end
 
 end
index 70934553f24d2679b5d42393a14357a4daec85cd..74d2aea80a77027197638134647e696cc357d0c0 100644 (file)
@@ -15,6 +15,7 @@ Server::Application.routes.draw do
         get 'used_by', on: :member
       end
       resources :groups do
+        get 'contents', on: :collection
         get 'contents', on: :member
       end
       resources :humans
index 0b7602942af27cdf087079ca5b319b450694121c..176e8e1129969e96fbf1ddd18f36ef9a06ee94fd 100644 (file)
@@ -83,6 +83,26 @@ class Arvados::V1::GroupsControllerTest < ActionController::TestCase
     check_project_contents_response
   end
 
+  test 'list objects across multiple projects' do
+    authorize_with :project_viewer
+    get :contents, {
+      format: :json,
+      include_linked: false,
+      filters: [['uuid', 'is_a', 'arvados#specimen']]
+    }
+    assert_response :success
+    found_uuids = json_response['items'].collect { |i| i['uuid'] }
+    [[:in_aproject, true],
+     [:in_asubproject, true],
+     [:owned_by_private_group, false]].each do |specimen_fixture, should_find|
+      if should_find
+        assert_includes found_uuids, specimens(specimen_fixture).uuid, "did not find specimen fixture '#{specimen_fixture}'"
+      else
+        refute_includes found_uuids, specimens(specimen_fixture).uuid, "found specimen fixture '#{specimen_fixture}'"
+      end
+    end
+  end
+
   # Even though the project_viewer tests go through other controllers,
   # I'm putting them here so they're easy to find alongside the other
   # project tests.