diff --git a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee index f07430279..dfdc5ff0b 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_form.js.coffee @@ -753,6 +753,11 @@ class App.ControllerForm extends App.Controller $('#' + attribute.id ).parent().css('height', 'auto') App.Delay.set( a, 120, undefined, 'tags' ) + # user + else if attribute.tag is 'user_autocompletion' + completion = new App.UserOrganizationAutocompletion( attribute: attribute ) + item = completion.element() + # autocompletion else if attribute.tag is 'autocompletion' item = $( App.view('generic/autocompletion')( attribute: attribute ) ) diff --git a/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee index e837ce821..20516b27e 100644 --- a/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee +++ b/app/assets/javascripts/app/controllers/agent_ticket_create.js.coffee @@ -4,13 +4,9 @@ class App.TicketCreate extends App.Controller events: 'click .type-tabs .tab': 'changeFormType' - 'click .customer_new': 'userNew' 'submit form': 'submit' 'click .submit': 'submit' 'click .cancel': 'cancel' - 'hide.bs.dropdown .js-recipientDropdown': 'hideOrganisationMembers' - 'click .js-organisation': 'showOrganisationMembers' - 'click .js-back': 'hideOrganisationMembers' constructor: (params) -> super @@ -40,61 +36,6 @@ class App.TicketCreate extends App.Controller @log 'notice', 'error', defaults @render(defaults) - showOrganisationMembers: (e) => - e.stopPropagation() - - listEntry = $(e.currentTarget) - organisationId = listEntry.data('organisation') - - @recipientList = @$('.recipientList') - @organisationList = @$("##{ organisationId }") - - # move organisation-list to the right and slide it in - - $.Velocity.hook(@organisationList, 'translateX', '100%') - @organisationList.removeClass('hide') - - @organisationList.velocity - properties: - translateX: 0 - options: - speed: 300 - - # fade out list - - @recipientList.velocity - properties: - translateX: '-100%' - options: - speed: 300 - complete: => @recipientList.height(@organisationList.height()) - - hideOrganisationMembers: (e) => - e && e.stopPropagation() - - return if !@organisationList - - # fade list back in - - @recipientList.velocity - properties: - translateX: 0 - options: - speed: 300 - - # reset list height - - @recipientList.height('') - - # slide out organisation-list and hide it - - @organisationList.velocity - properties: - translateX: '100%' - options: - speed: 300 - complete: => @organisationList.addClass('hide') - changeFormType: (e) => type = $(e.target).data('type') if !type @@ -353,12 +294,6 @@ class App.TicketCreate extends App.Controller params: params ) - userNew: (e) => - e.preventDefault() - new UserNew( - create_screen: @ - ) - cancel: (e) -> e.preventDefault() @navigate '#' @@ -545,68 +480,6 @@ class Sidebar extends App.Controller items: items ) -class UserNew extends App.ControllerModal - constructor: -> - super - @head = 'New User' - @cancel = true - @button = true - - controller = new App.ControllerForm( - el: @el.find('#form-user') - model: App.User - screen: 'edit' - autofocus: true - ) - - @show( controller.form ) - - onSubmit: (e) -> - - e.preventDefault() - params = @formParam(e.target) - - # if no login is given, use emails as fallback - if !params.login && params.email - params.login = params.email - - # find role_id - if !params.role_ids || _.isEmpty( params.role_ids ) - role = App.Role.findByAttribute( 'name', 'Customer' ) - params.role_ids = role.id - @log 'notice', 'updateAttributes', params - - user = new App.User - user.load(params) - - errors = user.validate() - if errors - @log 'error', errors - @formValidate( form: e.target, errors: errors ) - return - - # save user - ui = @ - user.save( - done: -> - - # force to reload object - callbackReload = (user) -> - realname = user.displayName() - if user.email - realname = "#{ realname } <#{ user.email }>" - ui.create_screen.el.find('[name=customer_id]').val( user.id ) - ui.create_screen.el.find('[name=customer_id_autocompletion]').val( realname ) - - # start customer info controller - ui.userInfo( user_id: user.id ) - ui.hide() - App.User.full( @id, callbackReload , true ) - - fail: -> - ui.hide() - ) - class Router extends App.ControllerPermanent constructor: (params) -> super diff --git a/app/assets/javascripts/app/controllers/layout_ref.js.coffee b/app/assets/javascripts/app/controllers/layout_ref.js.coffee index bf0eca142..d5f91dfae 100644 --- a/app/assets/javascripts/app/controllers/layout_ref.js.coffee +++ b/app/assets/javascripts/app/controllers/layout_ref.js.coffee @@ -10,6 +10,11 @@ App.Config.set( 'layout_ref', Index, 'Routes' ) class Content extends App.ControllerContent + events: + 'hide.bs.dropdown .js-recipientDropdown': 'hideOrganisationMembers' + 'click .js-organisation': 'showOrganisationMembers' + 'click .js-back': 'hideOrganisationMembers' + constructor: -> super @render() @@ -35,6 +40,60 @@ class Content extends App.ControllerContent render: -> @html App.view('layout_ref/content')() + showOrganisationMembers: (e) => + e.stopPropagation() + + listEntry = $(e.currentTarget) + organisationId = listEntry.data('organisation-id') + + @recipientList = @$('.recipientList') + @organisationList = @$("##{ organisationId }") + + # move organisation-list to the right and slide it in + + $.Velocity.hook(@organisationList, 'translateX', '100%') + @organisationList.removeClass('hide') + + @organisationList.velocity + properties: + translateX: 0 + options: + speed: 300 + + # fade out list + + @recipientList.velocity + properties: + translateX: '-100%' + options: + speed: 300 + complete: => @recipientList.height(@organisationList.height()) + + hideOrganisationMembers: (e) => + e && e.stopPropagation() + + return if !@organisationList + + # fade list back in + + @recipientList.velocity + properties: + translateX: 0 + options: + speed: 300 + + # reset list height + + @recipientList.height('') + + # slide out organisation-list and hide it + @organisationList.velocity + properties: + translateX: '100%' + options: + speed: 300 + complete: => @organisationList.addClass('hide') + App.Config.set( 'layout_ref/content', Content, 'Routes' ) diff --git a/app/assets/javascripts/app/controllers/ticket_customer.js.coffee b/app/assets/javascripts/app/controllers/ticket_customer.js.coffee index 5d09a51b7..34fbded2e 100644 --- a/app/assets/javascripts/app/controllers/ticket_customer.js.coffee +++ b/app/assets/javascripts/app/controllers/ticket_customer.js.coffee @@ -2,15 +2,13 @@ class App.TicketCustomer extends App.ControllerModal constructor: -> super configure_attributes = [ - { name: 'customer_id', display: 'Customer', tag: 'autocompletion', type: 'text', limit: 100, null: false, relation: 'User', class: 'span5', autocapitalize: false, help: 'Select the new customer of the Ticket.', source: @apiPath + '/users/search', minLengt: 2 }, + { name: 'customer_id', display: 'Customer', tag: 'user_autocompletion', null: false, placeholder: 'Enter Person or Organisation/Company', minLengt: 2, disableCreateUser: true }, ] controller = new App.ControllerForm( - model: { + model: configure_attributes: configure_attributes, - className: 'update', - }, - autofocus: true, + autofocus: true ) @head = 'Change Customer' @close = true 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 new file mode 100644 index 000000000..308f29d7c --- /dev/null +++ b/app/assets/javascripts/app/lib/app_post/user_organization_autocompletion.js.coffee @@ -0,0 +1,257 @@ +class App.UserOrganizationAutocompletion extends App.Controller + events: + 'hide.bs.dropdown .js-recipientDropdown': 'hideOrganisationMembers' + 'click .js-organisation': 'showOrganisationMembers' + 'click .js-back': 'hideOrganisationMembers' + 'click .js-user': 'selectUser' + 'click .js-user-new': 'newUser' + + constructor: (params) -> + super + + @key = Math.floor( Math.random() * 999999 ).toString() + + if !@attribute.source + @attribute.source = @apiPath + '/search_user_org' + @build() + + element: => + @el + + selectUser: (e) -> + userId = $(e.target).parents('.recipientList-entry').data('user-id') + if !userId + userId = $(e.target).data('user-id') + + @el.find('[name="' + @attribute.name + '"]').val( userId ).trigger('change') + + setUser: -> + userId = @el.find('[name="' + @attribute.name + '"]').val() + return if !userId + user = App.User.find(userId) + name = user.displayName() + if user.email + name += " <#{user.email}>" + @el.find('[name="' + @attribute.name + '_completion"]').val( name ).trigger('change') + + if @callback + @callback(userId) + + buildOrganizationItem: (organization) => + App.view('generic/user_search/item_organization')( + organization: organization + ) + + buildOrganizationMembers: (organization) => + organizationMemebers = $( App.view('generic/user_search/item_organization_members')( + organization: organization + ) ) + for userId in organization.member_ids + user = App.User.fullLocal(userId) + organizationMemebers.append( @buildUserItem(user) ) + + buildUserItem: (user) => + App.view('generic/user_search/item_user')( + user: user + ) + + buildUserNew: => + App.view('generic/user_search/new_user')() + + build: => + @el.html App.view('generic/user_search/input')( + attribute: @attribute + ) + @el.find('[name="' + @attribute.name + '"]').on( + 'change', + (e) => + @setUser() + ) + + @el.find('[name="' + @attribute.name + '_completion"]').on( + 'keyup', + (e) => + item = $(e.target).val() + + #@log('CC', e.keyCode, item) + + # clean input field on ESC + if e.keyCode is 27 + $(e.target).val('') + item = '' + + # ignore arrow keys + return if e.keyCode is 37 + return if e.keyCode is 38 + return if e.keyCode is 39 + return if e.keyCode is 40 + + # ignore shift + return if e.keyCode is 16 + + # ignore ctrl + return if e.keyCode is 17 + + # ignore alt + return if e.keyCode is 18 + + # hide dropdown + @el.find('.recipientList').html('') + @el.find('.recipientList-organisationMembers').remove() + if !item && !@attribute.disableCreateUser + @el.find('.recipientList').append( @buildUserNew() ) + + # show dropdown + if item && ( !@attribute.minLengt || @attribute.minLengt <= item.length ) + execute = => @searchUser(item) + @delay( execute, 400, 'userSearch' ) + ) + + searchUser: (term) => + + @ajax( + id: 'searchUser' + @key + type: 'GET' + url: @attribute.source + data: + query: term + processData: true + success: (data, status, xhr) => + # load assets + App.Collection.loadAssets( data.assets ) + + # build markup + for item in data.result + + # organization + if item.type is 'Organization' + organization = App.Organization.fullLocal( item.id ) + @el.find('.recipientList').append( @buildOrganizationItem(organization) ) + + # users of organization + if organization.member_ids + @el.find('.dropdown-menu').append( @buildOrganizationMembers(organization) ) + + # users + if item.type is 'User' + user = App.User.fullLocal( item.id ) + @el.find('.recipientList').append( @buildUserItem(user) ) + + if !@attribute.disableCreateUser + @el.find('.recipientList').append( @buildUserNew() ) + ) + + showOrganisationMembers: (e) => + e.stopPropagation() + + listEntry = $(e.currentTarget) + organisationId = listEntry.data('organisation-id') + + @recipientList = @$('.recipientList') + @organisationList = @$("##{ organisationId }") + + # move organisation-list to the right and slide it in + + $.Velocity.hook(@organisationList, 'translateX', '100%') + @organisationList.removeClass('hide') + + @organisationList.velocity + properties: + translateX: 0 + options: + speed: 300 + + # fade out list + @recipientList.velocity + properties: + translateX: '-100%' + options: + speed: 300 + complete: => @recipientList.height(@organisationList.height()) + + hideOrganisationMembers: (e) => + e && e.stopPropagation() + + return if !@organisationList + + # fade list back in + @recipientList.velocity + properties: + translateX: 0 + options: + speed: 300 + + # reset list height + + @recipientList.height('') + + # slide out organisation-list and hide it + @organisationList.velocity + properties: + translateX: '100%' + options: + speed: 300 + complete: => @organisationList.addClass('hide') + + newUser: (e) => + e.preventDefault() + new UserNew( + parent: @ + ) + +class UserNew extends App.ControllerModal + constructor: -> + super + @head = 'New User' + @cancel = true + @button = true + + controller = new App.ControllerForm( + el: @el.find('#form-user') + model: App.User + screen: 'edit' + autofocus: true + ) + + @show( controller.form ) + + onSubmit: (e) -> + + e.preventDefault() + params = @formParam(e.target) + + # if no login is given, use emails as fallback + if !params.login && params.email + params.login = params.email + + # find role_id + if !params.role_ids || _.isEmpty( params.role_ids ) + role = App.Role.findByAttribute( 'name', 'Customer' ) + params.role_ids = role.id + @log 'notice', 'updateAttributes', params + + user = new App.User + user.load(params) + + errors = user.validate() + if errors + @log 'error', errors + @formValidate( form: e.target, errors: errors ) + return + + # save user + ui = @ + user.save( + done: -> + + # force to reload object + callbackReload = (user) -> + ui.parent.el.find('[name=customer_id]').val( user.id ).trigger('change') + + # start customer info controller + ui.hide() + App.User.full( @id, callbackReload , true ) + + fail: -> + ui.hide() + ) \ No newline at end of file diff --git a/app/assets/javascripts/app/views/agent_ticket_create.jst.eco b/app/assets/javascripts/app/views/agent_ticket_create.jst.eco index 6cce00c13..3074bec7d 100644 --- a/app/assets/javascripts/app/views/agent_ticket_create.jst.eco +++ b/app/assets/javascripts/app/views/agent_ticket_create.jst.eco @@ -25,189 +25,7 @@
-
- - -
@@ -216,7 +34,6 @@
-
<%- @T( 'Cancel & Go Back' ) %> diff --git a/app/assets/javascripts/app/views/generic/user_search/input.jst.eco b/app/assets/javascripts/app/views/generic/user_search/input.jst.eco new file mode 100644 index 000000000..ba7f90465 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/user_search/input.jst.eco @@ -0,0 +1,12 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/user_search/item_organization.jst.eco b/app/assets/javascripts/app/views/generic/user_search/item_organization.jst.eco new file mode 100644 index 000000000..be1c06804 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/user_search/item_organization.jst.eco @@ -0,0 +1,10 @@ +
  • +
    +
    +
    +
    + <%= @organization.displayName() %> + - <%= @organization.member_ids.length %> <%- @T('People') %> +
    +
    +
  • \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/user_search/item_organization_members.jst.eco b/app/assets/javascripts/app/views/generic/user_search/item_organization_members.jst.eco new file mode 100644 index 000000000..c3bf75a00 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/user_search/item_organization_members.jst.eco @@ -0,0 +1,10 @@ + \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/user_search/item_user.jst.eco b/app/assets/javascripts/app/views/generic/user_search/item_user.jst.eco new file mode 100644 index 000000000..ff6b340c9 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/user_search/item_user.jst.eco @@ -0,0 +1,11 @@ +
  • +
    +
    +
    +
    + <%= @user.displayName() %> + <% if @user.organization: %> + - <%= @user.organization.displayName() %> + <% end %> +
    +
  • \ No newline at end of file diff --git a/app/assets/javascripts/app/views/generic/user_search/new_user.jst.eco b/app/assets/javascripts/app/views/generic/user_search/new_user.jst.eco new file mode 100644 index 000000000..2bdddbd24 --- /dev/null +++ b/app/assets/javascripts/app/views/generic/user_search/new_user.jst.eco @@ -0,0 +1,8 @@ +
  • +
    +
    +
    +
    + <%- @T('Create new Customer') %> +
    +
  • \ No newline at end of file diff --git a/app/assets/javascripts/app/views/layout_ref/content.jst.eco b/app/assets/javascripts/app/views/layout_ref/content.jst.eco index 757be48d7..2ddeb5226 100644 --- a/app/assets/javascripts/app/views/layout_ref/content.jst.eco +++ b/app/assets/javascripts/app/views/layout_ref/content.jst.eco @@ -40,6 +40,203 @@ + +
    + +

    User Selection

    + + +
    + + + + +
    + +

    Headlines

    diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb index b30cb0392..6a7225d81 100644 --- a/app/controllers/search_controller.rb +++ b/app/controllers/search_controller.rb @@ -3,6 +3,70 @@ class SearchController < ApplicationController before_filter :authentication_check + # GET /api/v1/search_user_org + def search_user_org + + # enable search only for agents and admins + if !current_user.is_role('Agent') && !current_user.is_role('Admin') + response_access_deny + return true + end + + # get params + query = params[:query] + limit = params[:limit] || 10 + + # try search index backend + assets = {} + result = [] + if SearchIndexBackend.enabled? + items = SearchIndexBackend.search( query, limit, ['User', 'Organization'] ) + items.each { |item| + require item[:type].to_filename + record = Kernel.const_get( item[:type] ).find( item[:id] ) + assets = record.assets(assets) + result.push item + } + else + # do query + users = User.search( + :query => query, + :limit => limit, + :current_user => current_user, + ) + user_result = [] + users.each do |user| + item = { + :id => user.id, + :type => user.class.to_s + } + result.push item + assets = user.assets(assets) + end + + organizations = Organization.search( + :query => query, + :limit => limit, + :current_user => current_user, + ) + + organization_result = [] + organizations.each do |organization| + item = { + :id => organization.id, + :type => organization.class.to_s + } + result.push item + assets = organization.assets(assets) + end + end + + render :json => { + :assets => assets, + :result => result, + } + end + # GET /api/v1/search def search diff --git a/app/models/organization/search.rb b/app/models/organization/search.rb index a33220457..d48e5ce15 100644 --- a/app/models/organization/search.rb +++ b/app/models/organization/search.rb @@ -30,10 +30,10 @@ returns # try search index backend if SearchIndexBackend.enabled? - ids = SearchIndexBackend.search( query, limit, 'Organization' ) + items = SearchIndexBackend.search( query, limit, 'Organization' ) organizations = [] - ids.each { |id| - organizations.push Organization.lookup( :id => id ) + items.each { |item| + organizations.push Organization.lookup( :id => item[:id] ) } return organizations end diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb index 0d9b5c113..84a4e27eb 100644 --- a/app/models/ticket/search.rb +++ b/app/models/ticket/search.rb @@ -78,13 +78,17 @@ returns query_extention['bool']['must'].push access_condition end - ids = SearchIndexBackend.search( query, limit, 'Ticket', query_extention ) + items = SearchIndexBackend.search( query, limit, 'Ticket', query_extention ) if !full + ids = [] + items.each {|item| + ids.push item[:id] + } return ids end tickets = [] - ids.each { |id| - tickets.push Ticket.lookup( :id => id ) + items.each { |item| + tickets.push Ticket.lookup( :id => item[:id] ) } return tickets end diff --git a/app/models/user/search.rb b/app/models/user/search.rb index 8cae405bc..58e1ecdb6 100644 --- a/app/models/user/search.rb +++ b/app/models/user/search.rb @@ -4,22 +4,6 @@ module User::Search =begin -search tickets - - result = Ticket.search( - :current_user => User.find(123), - :query => 'search something', - :limit => 15, - ) - -returns - - result = [ticket_model1, ticket_model2] - -=end - -=begin - search user result = User.search( @@ -46,10 +30,10 @@ returns # try search index backend if SearchIndexBackend.enabled? - ids = SearchIndexBackend.search( query, limit, 'User' ) + items = SearchIndexBackend.search( query, limit, 'User' ) users = [] - ids.each { |id| - users.push User.lookup( :id => id ) + items.each { |item| + users.push User.lookup( :id => item[:id] ) } return users end diff --git a/config/routes/search.rb b/config/routes/search.rb index ed877fd92..dc5e42916 100644 --- a/config/routes/search.rb +++ b/config/routes/search.rb @@ -2,6 +2,9 @@ Zammad::Application.routes.draw do api_path = Rails.configuration.api_path # search - match api_path + '/search', :to => 'search#search', :via => [:get, :post] + 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] end \ No newline at end of file diff --git a/db/migrate/20140919000001_update_object_manager2.rb b/db/migrate/20140919000001_update_object_manager2.rb new file mode 100644 index 000000000..6380f05c6 --- /dev/null +++ b/db/migrate/20140919000001_update_object_manager2.rb @@ -0,0 +1,38 @@ +class UpdateObjectManager2 < ActiveRecord::Migration + def up + + ObjectManager::Attribute.add( + :object => 'Ticket', + :name => 'customer_id', + :display => 'Customer', + :data_type => 'user_autocompletion', + :data_option => { + :autocapitalize => false, + :multiple => false, + :null => false, + :limit => 200, + :placeholder => 'Enter Person or Organisation/Company', + :minLengt => 2, + :translate => false, + }, + :editable => false, + :active => true, + :screens => { + :create_top => { + :Agent => { + :null => false, + }, + }, + :edit => {}, + }, + :pending_migration => false, + :position => 10, + :created_by_id => 1, + :updated_by_id => 1, + ) + + end + + def down + end +end diff --git a/lib/search_index_backend.rb b/lib/search_index_backend.rb index 9518f5fb3..7d69d8d20 100644 --- a/lib/search_index_backend.rb +++ b/lib/search_index_backend.rb @@ -113,8 +113,25 @@ remove whole data from index return search result + result = SearchIndexBackend.search( 'search query', limit, ['User', 'Organization'] ) + result = SearchIndexBackend.search( 'search query', limit, 'User' ) + result = [ + { + :id => 123, + :type => 'User', + }, + { + :id => 125, + :type => 'User', + }, + { + :id => 15, + :type => 'Organization', + } + ] + =end def self.search( query, limit = 10, index = nil, query_extention = {} ) @@ -123,7 +140,11 @@ return search result url = build_url() return if !url if index - url += "/#{index}/_search" + if index.class == Array + url += "/#{index.join(',')}/_search" + else + url += "/#{index}/_search" + end else url += '/_search' end @@ -176,7 +197,11 @@ return search result return ids if !data['hits']['hits'] data['hits']['hits'].each { |item| puts "... #{item['_type'].to_s} #{item['_id'].to_s}" - ids.push item['_id'] + data = { + :id => item['_id'], + :type => item['_type'], + } + ids.push data } ids end