From 804e9ddcfea7711236e8467d28c315d111a1be48 Mon Sep 17 00:00:00 2001 From: Thorsten Eckel Date: Wed, 5 Aug 2015 11:47:00 +0200 Subject: [PATCH] - Added SearchableAjaxSelect. - Refactored search_controller. --- .../app/controllers/layout_ref.js.coffee | 47 +++--- ...user_organization_autocompletion.js.coffee | 2 +- .../z_searchable_ajax_select.js.coffee | 77 ++++++++- .../app/views/layout_ref/inputs.jst.eco | 4 + app/controllers/search_controller.rb | 150 ++++++++---------- config/routes/search.rb | 7 +- 6 files changed, 178 insertions(+), 109 deletions(-) diff --git a/app/assets/javascripts/app/controllers/layout_ref.js.coffee b/app/assets/javascripts/app/controllers/layout_ref.js.coffee index aef80a862..f34fe78df 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.js.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.js.coffee @@ -375,7 +375,7 @@ class LayoutRefCommunicationReply extends App.ControllerContent for i in [0..100] setTimeout @updateUploadProgress, i*duration/100 , i - setTimeout (=> + setTimeout (=> callback() @renderAttachment(fileName, fileSize) ), duration @@ -584,7 +584,7 @@ class ImportWizard extends App.ControllerWizard @render() # wait 500 ms after the last user input before we check the link - @otrsLink.on 'input', _.debounce(@checkOtrsLink, 600) + @otrsLink.on 'input', _.debounce(@checkOtrsLink, 600) checkOtrsLink: (e) => if @otrsLink.val() is "" @@ -852,14 +852,14 @@ class highlightRef extends App.ControllerContent # containing the offsets and the highlight classes # # we have to check how it works with having open several tickets – it might break - # + # # if classes can be changed in the admin interface # we have to watch out to not end up with empty highlight classes storeHighlights: -> localStorage['highlights'] = @highlighter.serialize() # the colors is set via css classes (can't do it inline with rangy) - # thus we have to create a stylesheet if the colors + # thus we have to create a stylesheet if the colors # can be changed in the admin interface addClassApplier: (entry) -> @highlighter.addClassApplier rangy.createCssClassApplier(@highlightClassPrefix + entry.name) @@ -884,7 +884,7 @@ class highlightRef extends App.ControllerContent activate: -> selection = rangy.getSelection() - # if there's already something selected, + # if there's already something selected, # don't go into highlight mode # just toggle the selected if !selection.isCollapsed @@ -914,10 +914,10 @@ class highlightRef extends App.ControllerContent onMouseUp: (e) => @toggleHighlightAtSelection $(e.currentTarget).closest @articles.selector - # + # # toggle Highlight # ================ - # + # # - only works when the selection starts and ends inside an article # - clears highlights in selection # - or highlights the selection @@ -931,7 +931,7 @@ class highlightRef extends App.ControllerContent else @highlighter.highlightSelection @highlightClass, selection: selection - containerElementId: article.get(0).id + containerElementId: article.get(0).id # remove selection selection.removeAllRanges() @@ -1096,7 +1096,7 @@ class cluesRef extends App.ControllerContent showWindow: => @modalWindow.velocity - properties: + properties: scale: [1, 0.2] opacity: [1, 0] options: @@ -1105,7 +1105,7 @@ class cluesRef extends App.ControllerContent hideWindow: (callback) => @modalWindow.velocity - properties: + properties: scale: [0.2, 1] opacity: 0 options: @@ -1131,7 +1131,7 @@ class cluesRef extends App.ControllerContent if target.right + modal.width <= maxWidth left = target.right position = 'right' - else + else # place left left = target.left - modal.width position = 'left' @@ -1208,7 +1208,7 @@ class cluesRef extends App.ControllerContent getVisibleBoundingBox: (el) -> ### - getBoundingClientRect doesn't take + getBoundingClientRect doesn't take absolute-positioned child nodes into account ### @@ -1412,7 +1412,7 @@ class schedulersRef extends App.ControllerContent switch items.length when 1 then return items[0] when 2 then return "#{ items[0] } and #{ items[1] }" - else + else return "#{ items.slice(0, -1).join(', ') } and #{ items[items.length-1] }" App.Config.set( 'layout_ref/schedulers', schedulersRef, 'Routes' ) @@ -1431,11 +1431,22 @@ class InputsRef extends App.ControllerContent # selectable search searchableSelectObject = new App.SearchableSelect attribute: - name: 'project-name' - id: 'project-name-123' + name: 'project-name' + id: 'project-name-123' placeholder: 'Enter Project Name' - options: [{"value":0,"name":"Apple"},{"value":1,"name":"Microsoft","selected":true},{"value":2,"name":"Google"},{"value":3,"name":"Deutsche Bahn"},{"value":4,"name":"Sparkasse"},{"value":5,"name":"Deutsche Post"},{"value":6,"name":"Mitfahrzentrale"},{"value":7,"name":"Starbucks"},{"value":8,"name":"Mac Donalds"},{"value":9,"name":"Flixbus"},{"value":10,"name":"Betahaus"},{"value":11,"name":"Bruno Banani"},{"value":12,"name":"Alpina"},{"value":13,"name":"Samsung"},{"value":14,"name":"ChariTea"},{"value":15,"name":"fritz-kola"},{"value":16,"name":"Vitamin Water"},{"value":17,"name":"Znuny"},{"value":18,"name":"Max & Moritz"}] - @$('.searchableSelectPlaceholder').replaceWith( searchableSelectObject.el ) + options: [{"value":0,"name":"Apple"},{"value":1,"name":"Microsoft","selected":true},{"value":2,"name":"Google"},{"value":3,"name":"Deutsche Bahn"},{"value":4,"name":"Sparkasse"},{"value":5,"name":"Deutsche Post"},{"value":6,"name":"Mitfahrzentrale"},{"value":7,"name":"Starbucks"},{"value":8,"name":"Mac Donalds"},{"value":9,"name":"Flixbus"},{"value":10,"name":"Betahaus"},{"value":11,"name":"Bruno Banani"},{"value":12,"name":"Alpina"},{"value":13,"name":"Samsung"},{"value":14,"name":"ChariTea"},{"value":15,"name":"fritz-kola"},{"value":16,"name":"Vitamin Water"},{"value":17,"name":"Znuny"},{"value":18,"name":"Max & Moritz"}] + @$('.searchableSelectPlaceholder').replaceWith( searchableSelectObject.element() ) + + # selectable search + searchableAjaxSelectObject = new App.SearchableAjaxSelect + attribute: + name: 'user' + id: 'user-123' + placeholder: 'Enter User' + limt: 10 + object: 'User' + + @$('.searchableAjaxSelectPlaceholder').replaceWith( searchableAjaxSelectObject.element() ) # time and timeframe @$('.time').timepicker() @@ -1515,7 +1526,7 @@ class calendarSubscriptionsRef extends App.ControllerContent switch items.length when 1 then return items[0] when 2 then return "#{ items[0] } and #{ items[1] }" - else + else return "#{ items.slice(0, -1).join(', ') } and #{ items[items.length-1] }" diff --git a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.js.coffee b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.js.coffee index 4c13dc901..45a1c4ef7 100644 --- a/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.js.coffee +++ b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.js.coffee @@ -14,7 +14,7 @@ class App.UserOrganizationAutocompletion extends App.Controller @key = Math.floor( Math.random() * 999999 ).toString() if !@attribute.source - @attribute.source = @apiPath + '/search_user_org' + @attribute.source = @apiPath + '/search/user-organization' @build() element: => diff --git a/app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee b/app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee index 1ef08cd5e..3abaf241b 100644 --- a/app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee +++ b/app/assets/javascripts/app/lib/app_post/z_searchable_ajax_select.js.coffee @@ -3,12 +3,81 @@ class App.SearchableAjaxSelect extends App.SearchableSelect onInput: (event) => super - # send ajax request @query + # convert requested object + # e.g. Ticket to ticket or AnotherObject to another_object + objectString = underscored( @options.attribute.object ) - onAjaxResponse: (data) => + # create common accessors + @apiPath = App.Config.get('api_path') + + # create cache and cache key + @searchResultCache = @searchResultCache || {} + + @cacheKey = "#{objectString}+#{@query}" + + # use cache for search result + if @searchResultCache[@cacheKey] + return @onAjaxResponse( @searchResultCache[@cacheKey] ) + + # add timout for loader icon + clearTimeout @loaderTimeoutId + @loaderTimeoutId = setTimeout @showLoader, 1000 + + # start search request and update options + App.Ajax.request( + id: @options.attribute.id + type: 'GET' + url: "#{@apiPath}/search/#{objectString}" + data: + query: @query + limit: @options.attribute.limit + processData: true + success: @onAjaxResponse + ) + + onAjaxResponse: (data, status, xhr) => + + # clear timout and remove loader icon + clearTimeout @loaderTimeoutId + @el.removeClass('is-loading') + + # cache search result + @searchResultCache[@cacheKey] = data + + # load assets + App.Collection.loadAssets( data.assets ) + + # get options from search result + options = [] + for object in data.result + if object.type is 'Ticket' + ticket = App.Ticket.find( object.id ) + data = + name: "##{ticket.number} - #{ticket.title}" + value: ticket.id + options.push data + else if object.type is 'User' + user = App.User.find( object.id ) + data = + name: "#{user.displayName()}" + value: user.id + options.push data + else if object.type is 'Organization' + organization = App.Organization.find( object.id ) + data = + name: "#{organization.displayName()}" + value: organization.id + options.push data + + # fill template with gathered options @optionsList.html App.view('generic/searchable_select_options') - options: data + options: options + # refresh elements @refreshElements() - @filterByQuery @query \ No newline at end of file + # execute filter + @filterByQuery @query + + showLoader: => + @el.addClass('is-loading') diff --git a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco index efa165875..fff57ff2b 100644 --- a/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/inputs.jst.eco @@ -66,6 +66,10 @@
+
+ +
+

Checkbox

diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index 3d2e79fc1..22312bb30 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -3,8 +3,9 @@ class SearchController < ApplicationController before_action :authentication_check - # GET /api/v1/search_user_org - def search_user_org + # GET|POST /api/v1/search/:objects + + def search_generic # enable search only for agents and admins if !current_user.role?(Z_ROLENAME_AGENT) && !current_user.role?(Z_ROLENAME_ADMIN) @@ -16,11 +17,17 @@ class SearchController < ApplicationController query = params[:query] limit = params[:limit] || 10 + # convert objects string into array of class names + # e.g. user-ticket-another_object = %w( User Ticket AnotherObject ) + objects = params[:objects].split('-').map { |object| + object.camelize + } + # try search index backend assets = {} result = [] if SearchIndexBackend.enabled? - items = SearchIndexBackend.search( query, limit, %w(User Organization) ) + items = SearchIndexBackend.search( query, limit, objects ) items.each { |item| require item[:type].to_filename record = Kernel.const_get( item[:type] ).find( item[:id] ) @@ -29,34 +36,23 @@ class SearchController < ApplicationController } else # do query - users = User.search( - query: query, - limit: limit, - current_user: current_user, - ) - users.each do |user| - item = { - id: user.id, - type: user.class.to_s - } - result.push item - assets = user.assets(assets) - end + objects.each { |object| - organizations = Organization.search( - query: query, - limit: limit, - current_user: current_user, - ) + found_objects = object.constantize.search( + query: query, + limit: limit, + current_user: current_user, + ) - organizations.each do |organization| - item = { - id: organization.id, - type: organization.class.to_s - } - result.push item - assets = organization.assets(assets) - end + found_objects.each do |found_object| + item = { + id: found_object.id, + type: found_object.class.to_s + } + result.push item + assets = found_object.assets(assets) + end + } end render json: { @@ -68,64 +64,54 @@ class SearchController < ApplicationController # GET /api/v1/search def search - # build result list - tickets = Ticket.search( - limit: params[:limit], - query: params[:term], - current_user: current_user, - ) - assets = {} - ticket_result = [] - tickets.each do |ticket| - assets = ticket.assets(assets) - ticket_result.push ticket.id - end + assets = {} + result = [] + objects = %w( Ticket User Organization ) + if SearchIndexBackend.enabled? - # do query - users = User.search( - query: params[:term], - limit: params[:limit], - current_user: current_user, - ) - user_result = [] - users.each do |user| - user_result.push user.id - assets = user.assets(assets) - end + found_objects = {} + items = SearchIndexBackend.search( params[:term], params[:limit], objects ) + items.each { |item| + require item[:type].to_filename + record = Kernel.const_get( item[:type] ).find( item[:id] ) + assets = record.assets(assets) - organizations = Organization.search( - query: params[:term], - limit: params[:limit], - current_user: current_user, - ) - - organization_result = [] - organizations.each do |organization| - organization_result.push organization.id - assets = organization.assets(assets) - end - - result = [] - if ticket_result[0] - data = { - name: 'Ticket', - ids: ticket_result, + found_objects[ item[:type] ] ||= [] + found_objects[ item[:type] ].push item[:id] } - result.push data - end - if user_result[0] - data = { - name: 'User', - ids: user_result, + + found_objects.each { |object, object_ids| + + data = { + name: object, + ids: object_ids, + } + result.push data } - result.push data - end - if organization_result[0] - data = { - name: 'Organization', - ids: organization_result, + else + + objects.each { |object| + + found_objects = object.constantize.search( + query: params[:term], + limit: params[:limit], + current_user: current_user, + ) + + object_ids = [] + found_objects.each do |found_object| + object_ids.push found_object.id + assets = found_object.assets(assets) + end + + next if object_ids.empty? + + data = { + name: object, + ids: object_ids, + } + result.push data } - result.push data end # return result diff --git a/config/routes/search.rb b/config/routes/search.rb index 827cd97db..44eed8c42 100644 --- a/config/routes/search.rb +++ b/config/routes/search.rb @@ -2,9 +2,8 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # search - match api_path + '/search', to: 'search#search', via: [:get, :post] - - # search_user_org - match api_path + '/search_user_org', to: 'search#search_user_org', via: [:get, :post] + match api_path + '/search', to: 'search#search', via: [:get, :post] + # search_generic + match api_path + '/search/:objects', to: 'search#search_generic', via: [:get, :post] end