diff --git a/app/assets/javascripts/app/controllers/_application_controller_table.coffee b/app/assets/javascripts/app/controllers/_application_controller_table.coffee index 32dbefa06..950dcddb3 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_table.coffee @@ -190,9 +190,8 @@ class App.ControllerTable extends App.Controller attribute.displayWidth = value @headers.push attribute else - # e.g. column: owner_id - rowWithoutId = item + '_id' - if attributeName is rowWithoutId + # e.g. column: owner_id or owner_ids + if attributeName is "#{item}_id" || attributeName is "#{item}_ids" headerFound = true if @headerWidth[attribute.name] attribute.displayWidth = @headerWidth[attribute.name] * @availableWidth @@ -350,7 +349,7 @@ class App.ControllerTable extends App.Controller for headerName in @headers if !hit position += 1 - if headerName.name is name || headerName.name is "#{name}_id" + if headerName.name is name || headerName.name is "#{name}_id" || headerName.name is "#{name}_ids" hit = true if hit diff --git a/app/assets/javascripts/app/index.coffee b/app/assets/javascripts/app/index.coffee index 953beb12c..933e01c90 100644 --- a/app/assets/javascripts/app/index.coffee +++ b/app/assets/javascripts/app/index.coffee @@ -179,85 +179,94 @@ class App extends Spine.Controller return '-' if item is undefined return '-' if item is '' return item if item is null - result = item + result = '' + items = [item] + if _.isArray(item) + items = item # lookup relation - if attributeConfig.relation || valueRef - if valueRef - item = valueRef - else - item = App[attributeConfig.relation].find(item) + for item in items + resultLocal = item + if attributeConfig.relation || valueRef + if valueRef + item = valueRef + else + item = App[attributeConfig.relation].find(item) - # if date is a object, get name of the object - isObject = false - if typeof item is 'object' - isObject = true - if item.displayNameLong - result = item.displayNameLong() - else if item.displayName - result = item.displayName() - else - result = item.name + # if date is a object, get name of the object + isObject = false + if typeof item is 'object' + isObject = true + if item.displayNameLong + resultLocal = item.displayNameLong() + else if item.displayName + resultLocal = item.displayName() + else + resultLocal = item.name - # execute callback on content - if attributeConfig.callback - result = attributeConfig.callback(result, attributeConfig) + # execute callback on content + if attributeConfig.callback + resultLocal = attributeConfig.callback(resultLocal, attributeConfig) - # text2html in textarea view - isHtmlEscape = false - if attributeConfig.tag is 'textarea' - isHtmlEscape = true - result = App.Utils.text2html(result) + # text2html in textarea view + isHtmlEscape = false + if attributeConfig.tag is 'textarea' + isHtmlEscape = true + resultLocal = App.Utils.text2html(resultLocal) - # remember, html snippets are already escaped - else if attributeConfig.tag is 'richtext' - isHtmlEscape = true + # remember, html snippets are already escaped + else if attributeConfig.tag is 'richtext' + isHtmlEscape = true - # fillup options - if !_.isEmpty(attributeConfig.options) - if attributeConfig.options[result] - result = attributeConfig.options[result] + # fillup options + if !_.isEmpty(attributeConfig.options) + if attributeConfig.options[resultLocal] + resultLocal = attributeConfig.options[resultLocal] - # transform boolean - if attributeConfig.tag is 'boolean' - if result is true - result = 'yes' - else if result is false - result = 'no' + # transform boolean + if attributeConfig.tag is 'boolean' + if resultLocal is true + resultLocal = 'yes' + else if resultLocal is false + resultLocal = 'no' - # translate content - if attributeConfig.translate || (isObject && item.translate && item.translate()) - isHtmlEscape = true - result = App.i18n.translateContent(result) + # translate content + if attributeConfig.translate || (isObject && item.translate && item.translate()) + isHtmlEscape = true + resultLocal = App.i18n.translateContent(resultLocal) - # transform date - if attributeConfig.tag is 'date' - isHtmlEscape = true - result = App.i18n.translateDate(result) + # transform date + if attributeConfig.tag is 'date' + isHtmlEscape = true + resultLocal = App.i18n.translateDate(resultLocal) - # transform input tel|url to make it clickable - if attributeConfig.tag is 'input' - if attributeConfig.type is 'tel' - result = "#{App.Utils.htmlEscape(result)}" - else if attributeConfig.type is 'url' - result = App.Utils.linkify(result) - else - result = App.Utils.htmlEscape(result) - isHtmlEscape = true + # transform input tel|url to make it clickable + if attributeConfig.tag is 'input' + if attributeConfig.type is 'tel' + resultLocal = "#{App.Utils.htmlEscape(resultLocal)}" + else if attributeConfig.type is 'url' + resultLocal = App.Utils.linkify(resultLocal) + else + resultLocal = App.Utils.htmlEscape(resultLocal) + isHtmlEscape = true - # use pretty time for datetime - else if attributeConfig.tag is 'datetime' - isHtmlEscape = true - timestamp = App.i18n.translateTimestamp(result) - escalation = false - cssClass = attributeConfig.class || '' - if cssClass.match 'escalation' - escalation = true - humanTime = App.PrettyDate.humanTime(result, escalation) - result = "" + # use pretty time for datetime + else if attributeConfig.tag is 'datetime' + isHtmlEscape = true + timestamp = App.i18n.translateTimestamp(resultLocal) + escalation = false + cssClass = attributeConfig.class || '' + if cssClass.match 'escalation' + escalation = true + humanTime = App.PrettyDate.humanTime(resultLocal, escalation) + resultLocal = "" - if !isHtmlEscape && typeof result is 'string' - result = App.Utils.htmlEscape(result) + if !isHtmlEscape && typeof resultLocal is 'string' + resultLocal = App.Utils.htmlEscape(resultLocal) + + if !_.isEmpty(result) + result += ', ' + result += resultLocal result diff --git a/app/assets/javascripts/app/models/overview.coffee b/app/assets/javascripts/app/models/overview.coffee index 029470430..df0f7af1b 100644 --- a/app/assets/javascripts/app/models/overview.coffee +++ b/app/assets/javascripts/app/models/overview.coffee @@ -1,11 +1,11 @@ class App.Overview extends App.Model - @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_id', 'order', 'group_by', 'active', 'updated_at' + @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at' @extend Spine.Model.Ajax @url: @apiPath + '/overviews' @configure_attributes = [ { name: 'name', display: 'Name', tag: 'input', type: 'text', limit: 100, 'null': false }, { name: 'link', display: 'Link', readonly: 1 }, - { name: 'role_id', display: 'Available for Role', tag: 'select', multiple: false, nulloption: true, null: false, relation: 'Role', translate: true }, + { name: 'role_ids', display: 'Available for Role', tag: 'column_select', multiple: true, nulloption: true, null: false, relation: 'Role', translate: true }, { name: 'user_ids', display: 'Available for User', tag: 'column_select', multiple: true, nulloption: false, null: true, relation: 'User', sortBy: 'firstname' }, { name: 'organization_shared', display: 'Only available for Users with shared Organization', tag: 'select', options: { true: 'yes', false: 'no' }, default: false, null: true }, { name: 'condition', display: 'Conditions for shown Tickets', tag: 'ticket_selector', null: false }, @@ -62,7 +62,7 @@ class App.Overview extends App.Model @configure_overview = [ 'name', 'link', - 'role', + 'role_ids', ] @description = ''' diff --git a/app/models/overview.rb b/app/models/overview.rb index f631aa13a..8b4c284ce 100644 --- a/app/models/overview.rb +++ b/app/models/overview.rb @@ -7,6 +7,7 @@ class Overview < ApplicationModel load 'overview/assets.rb' include Overview::Assets + has_and_belongs_to_many :roles, after_add: :cache_update, after_remove: :cache_update, class_name: 'Role' has_and_belongs_to_many :users, after_add: :cache_update, after_remove: :cache_update store :condition store :order diff --git a/app/models/ticket/overviews.rb b/app/models/ticket/overviews.rb index 1c9074aa0..6f9e0b4fd 100644 --- a/app/models/ticket/overviews.rb +++ b/app/models/ticket/overviews.rb @@ -19,11 +19,12 @@ returns current_user = data[:current_user] # get customer overviews + role_ids = User.joins(:roles).where(users: { id: current_user.id, active: true }, roles: { active: true }).pluck('roles.id') if current_user.permissions?('ticket.customer') overviews = if current_user.organization_id && current_user.organization.shared - Overview.where(role_id: current_user.role_ids, active: true).order(:prio) + Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio) else - Overview.where(role_id: current_user.role_ids, organization_shared: false, active: true).order(:prio) + Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true, organization_shared: false }).distinct('overview.id').order(:prio) end overviews_list = [] overviews.each { |overview| @@ -36,7 +37,7 @@ returns # get agent overviews return [] if !current_user.permissions?('ticket.agent') - overviews = Overview.where(role_id: current_user.role_ids, active: true).order(:prio) + overviews = Overview.joins(:roles).where(overviews_roles: { role_id: role_ids }, overviews: { active: true }).distinct('overview.id').order(:prio) overviews_list = [] overviews.each { |overview| user_ids = overview.user_ids diff --git a/db/migrate/20120101000010_create_ticket.rb b/db/migrate/20120101000010_create_ticket.rb index 35f134fc9..e8c68dd0f 100644 --- a/db/migrate/20120101000010_create_ticket.rb +++ b/db/migrate/20120101000010_create_ticket.rb @@ -196,7 +196,6 @@ class CreateTicket < ActiveRecord::Migration add_index :ticket_counters, [:generator], unique: true create_table :overviews do |t| - t.references :role, null: false t.column :name, :string, limit: 250, null: false t.column :link, :string, limit: 250, null: false t.column :prio, :integer, null: false @@ -212,6 +211,13 @@ class CreateTicket < ActiveRecord::Migration end add_index :overviews, [:name] + create_table :overviews_roles, id: false do |t| + t.integer :overview_id + t.integer :role_id + end + add_index :overviews_roles, [:overview_id] + add_index :overviews_roles, [:role_id] + create_table :overviews_users, id: false do |t| t.integer :overview_id t.integer :user_id diff --git a/db/migrate/20170419000002_overview_role_ids.rb b/db/migrate/20170419000002_overview_role_ids.rb new file mode 100644 index 000000000..255db825d --- /dev/null +++ b/db/migrate/20170419000002_overview_role_ids.rb @@ -0,0 +1,23 @@ +class OverviewRoleIds < ActiveRecord::Migration + def up + + # return if it's a new setup + return if !Setting.find_by(name: 'system_init_done') + + create_table :overviews_roles, id: false do |t| + t.integer :overview_id + t.integer :role_id + end + add_index :overviews_roles, [:overview_id] + add_index :overviews_roles, [:role_id] + Overview.connection.schema_cache.clear! + Overview.reset_column_information + Overview.all.each { |overview| + next if overview.role_id.blank? + overview.role_ids = [overview.role_id] + overview.save! + } + remove_column :overviews, :role_id + end + +end diff --git a/db/seeds.rb b/db/seeds.rb index 8257e7378..f762725ef 100644 --- a/db/seeds.rb +++ b/db/seeds.rb @@ -3367,7 +3367,7 @@ Overview.create_if_not_exists( name: 'My assigned Tickets', link: 'my_assigned', prio: 1000, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3394,7 +3394,7 @@ Overview.create_if_not_exists( name: 'Unassigned & Open', link: 'all_unassigned', prio: 1010, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3421,7 +3421,7 @@ Overview.create_if_not_exists( name: 'My pending reached Tickets', link: 'my_pending_reached', prio: 1020, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3453,7 +3453,7 @@ Overview.create_if_not_exists( name: 'Open', link: 'all_open', prio: 1030, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3476,7 +3476,7 @@ Overview.create_if_not_exists( name: 'Pending reached', link: 'all_pending_reached', prio: 1040, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3504,7 +3504,7 @@ Overview.create_if_not_exists( name: 'Escalated', link: 'all_escalated', prio: 1050, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.escalation_at' => { operator: 'within next (relative)', @@ -3529,7 +3529,7 @@ Overview.create_if_not_exists( name: 'My Tickets', link: 'my_tickets', prio: 1100, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -3555,7 +3555,7 @@ Overview.create_if_not_exists( name: 'My Organization Tickets', link: 'my_organization_tickets', prio: 1200, - role_id: overview_role.id, + role_ids: [overview_role.id], organization_shared: true, condition: { 'ticket.state_id' => { diff --git a/test/browser/admin_overview_test.rb b/test/browser/admin_overview_test.rb index d04081298..94a577ea7 100644 --- a/test/browser/admin_overview_test.rb +++ b/test/browser/admin_overview_test.rb @@ -16,8 +16,8 @@ class AdminOverviewTest < TestCase # add new overview overview_create( data: { - name: name, - role: 'Agent', + name: name, + roles: ['Agent'], selector: { 'Priority' => '1 low', }, @@ -28,8 +28,8 @@ class AdminOverviewTest < TestCase # edit overview overview_update( data: { - name: name, - role: 'Agent', + name: name, + roles: ['Agent'], selector: { 'State' => 'new', }, diff --git a/test/browser/agent_ticket_overview_level1_test.rb b/test/browser/agent_ticket_overview_level1_test.rb index 12a87c22d..4200c37a3 100644 --- a/test/browser/agent_ticket_overview_level1_test.rb +++ b/test/browser/agent_ticket_overview_level1_test.rb @@ -29,7 +29,7 @@ class AgentTicketOverviewLevel1Test < TestCase browser: browser1, data: { name: name1, - role: 'Agent', + roles: ['Agent'], selector: { 'Priority' => '1 low', }, @@ -40,7 +40,7 @@ class AgentTicketOverviewLevel1Test < TestCase browser: browser1, data: { name: name2, - role: 'Agent', + roles: ['Agent'], selector: { 'Priority' => '3 high', }, diff --git a/test/browser_test_helper.rb b/test/browser_test_helper.rb index 2c1022d69..6af218976 100644 --- a/test/browser_test_helper.rb +++ b/test/browser_test_helper.rb @@ -1574,8 +1574,8 @@ wait untill text in selector disabppears overview_create( browser: browser1, data: { - name: name, - role: 'Agent', + name: name, + roles: ['Agent'], selector: { 'Priority': '1 low', }, @@ -1616,13 +1616,19 @@ wait untill text in selector disabppears mute_log: true, ) end - if data[:role] - select( - browser: instance, - css: '.modal select[name="role_id"]', - value: data[:role], - mute_log: true, - ) + + if data[:roles] + 99.times do + begin + element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0] + break if !element + element.click + sleep 0.1 + end + end + data[:roles].each { |role| + instance.execute_script("$(\".modal [data-name=role_ids] .js-pool .js-option:not(.is-hidden):contains('#{role}')\").first().click()") + } end if data[:selector] @@ -1677,8 +1683,8 @@ wait untill text in selector disabppears overview_update( browser: browser1, data: { - name: name, - role: 'Agent', + name: name, + roles: ['Agent'], selector: { 'Priority': '1 low', }, @@ -1717,13 +1723,18 @@ wait untill text in selector disabppears mute_log: true, ) end - if data[:role] - select( - browser: instance, - css: '.modal select[name="role_id"]', - value: data[:role], - mute_log: true, - ) + if data[:roles] + 99.times do + begin + element = instance.find_elements(css: '.modal .js-selected[data-name=role_ids] .js-option:not(.is-hidden)')[0] + break if !element + element.click + sleep 0.1 + end + end + data[:roles].each { |role| + instance.execute_script("$(\".modal [data-name=role_ids] .js-pool .js-option:not(.is-hidden):contains('#{role}')\").first().click()") + } end if data[:selector] diff --git a/test/unit/assets_test.rb b/test/unit/assets_test.rb index 1c621ba07..6f042ab9e 100644 --- a/test/unit/assets_test.rb +++ b/test/unit/assets_test.rb @@ -351,8 +351,8 @@ class AssetsTest < ActiveSupport::TestCase name: 'my asset test', link: 'my_asset_test', prio: 1000, - role_id: overview_role.id, - user_ids: [ user4.id, user5.id ], + role_ids: [overview_role.id], + user_ids: [user4.id, user5.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -389,8 +389,8 @@ class AssetsTest < ActiveSupport::TestCase name: 'my asset test', link: 'my_asset_test', prio: 1000, - role_id: overview_role.id, - user_ids: [ user4.id ], + role_ids: [overview_role.id], + user_ids: [user4.id], condition: { 'ticket.state_id' => { operator: 'is', diff --git a/test/unit/ticket_overview_test.rb b/test/unit/ticket_overview_test.rb index 8530c9db6..555ebf023 100644 --- a/test/unit/ticket_overview_test.rb +++ b/test/unit/ticket_overview_test.rb @@ -105,7 +105,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My assigned Tickets', link: 'my_assigned', prio: 1000, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -132,7 +132,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'Unassigned & Open', link: 'all_unassigned', prio: 1010, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -158,7 +158,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My Tickets 2', link: 'my_tickets_2', prio: 1020, - role_id: overview_role.id, + role_ids: [overview_role.id], user_ids: [agent2.id], condition: { 'ticket.state_id' => { @@ -185,7 +185,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My Tickets only with Note', link: 'my_tickets_onyl_with_note', prio: 1030, - role_id: overview_role.id, + role_ids: [overview_role.id], user_ids: [agent1.id], condition: { 'article.type_id' => { @@ -214,7 +214,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My Tickets', link: 'my_tickets', prio: 1100, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is', @@ -240,7 +240,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My Organization Tickets', link: 'my_organization_tickets', prio: 1200, - role_id: overview_role.id, + role_ids: [overview_role.id], organization_shared: true, condition: { 'ticket.state_id' => { @@ -267,7 +267,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'My Organization Tickets (open)', link: 'my_organization_tickets_open', prio: 1200, - role_id: overview_role.id, + role_ids: [overview_role.id], user_ids: [customer2.id], organization_shared: true, condition: { @@ -297,7 +297,7 @@ class TicketOverviewTest < ActiveSupport::TestCase name: 'Not Shown Admin', link: 'not_shown_admin', prio: 9900, - role_id: overview_role.id, + role_ids: [overview_role.id], condition: { 'ticket.state_id' => { operator: 'is',