Fixed issue #1681 - Unable to re-order overviews in admin interface with over 100 overviews.

This commit is contained in:
Martin Edenhofer 2017-11-27 11:50:57 +01:00
parent ce1135a55c
commit 7599e8e17a
12 changed files with 503 additions and 66 deletions

View file

@ -148,24 +148,26 @@ class App.ControllerGenericIndex extends App.Controller
return item return item
) )
# show description button, only if content exists if !@table
showDescription = false
if App[ @genericObject ].description && !_.isEmpty(objects)
showDescription = true
@html App.view('generic/admin/index')( # show description button, only if content exists
head: @pageData.objects showDescription = false
notes: @pageData.notes if App[ @genericObject ].description && !_.isEmpty(objects)
buttons: @pageData.buttons showDescription = true
menus: @pageData.menus
showDescription: showDescription
)
# show description in content if no no content exists @html App.view('generic/admin/index')(
if _.isEmpty(objects) && App[ @genericObject ].description head: @pageData.objects
description = marked(App[ @genericObject ].description) notes: @pageData.notes
@$('.table-overview').html(description) buttons: @pageData.buttons
return menus: @pageData.menus
showDescription: showDescription
)
# show description in content if no no content exists
if _.isEmpty(objects) && App[ @genericObject ].description
description = marked(App[ @genericObject ].description)
@$('.table-overview').html(description)
return
# append content table # append content table
params = _.extend( params = _.extend(
@ -184,7 +186,10 @@ class App.ControllerGenericIndex extends App.Controller
}, },
@pageData.tableExtend @pageData.tableExtend
) )
new App.ControllerTable(params) if !@table
@table = new App.ControllerTable(params)
else
@table.update(objects: objects)
edit: (id, e) => edit: (id, e) =>
e.preventDefault() e.preventDefault()

View file

@ -23,17 +23,22 @@ class Index extends App.ControllerSubContent
] ]
container: @el.closest('.content') container: @el.closest('.content')
large: true large: true
dndCallback: => dndCallback: (e, item) =>
items = @el.find('table > tbody > tr') items = @el.find('table > tbody > tr')
order = [] prios = []
prio = 0 prio = 0
for item in items for item in items
prio += 1 prio += 1
id = $(item).data('id') id = $(item).data('id')
overview = App.Overview.find(id) prios.push [id, prio]
if overview.prio isnt prio
overview.prio = prio @ajax(
overview.save() id: 'overview_prio'
type: 'POST'
url: "#{@apiPath}/overviews_prio"
processData: true
data: JSON.stringify(prios: prios)
)
) )
App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin') App.Config.set('Overview', { prio: 2300, name: 'Overviews', parent: '#manage', target: '#manage/overviews', controller: Index, permission: ['admin.overview'] }, 'NavBarAdmin')

View file

@ -387,15 +387,17 @@ set new attributes of model (remove already available attributes)
=> =>
return if _.isEmpty(@SUBSCRIPTION_COLLECTION) return if _.isEmpty(@SUBSCRIPTION_COLLECTION)
App.Log.debug('Model', "server notify collection change #{@className}") App.Log.debug('Model', "server notify collection change #{@className}")
@fetchFull( callback = =>
-> @fetchFull(
clear: true ->
) clear: true
)
App.Delay.set(callback, 200, "full-#{@className}")
"Collection::Subscribe::#{@className}" "Collection::Subscribe::#{@className}"
) )
key = @className + '-' + Math.floor( Math.random() * 99999 ) key = "#{@className}-#{Math.floor(Math.random() * 99999)}"
@SUBSCRIPTION_COLLECTION[key] = callback @SUBSCRIPTION_COLLECTION[key] = callback
# fetch init collection # fetch init collection

View file

@ -1,5 +1,5 @@
class App.Overview extends App.Model class App.Overview extends App.Model
@configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'order', 'group_by', 'active', 'updated_at' @configure 'Overview', 'name', 'prio', 'condition', 'order', 'group_by', 'view', 'user_ids', 'organization_shared', 'role_ids', 'active'
@extend Spine.Model.Ajax @extend Spine.Model.Ajax
@url: @apiPath + '/overviews' @url: @apiPath + '/overviews'
@configure_attributes = [ @configure_attributes = [

View file

@ -30,7 +30,7 @@ Example:
=begin =begin
Resource: Resource:
GET /api/v1/overviews.json GET /api/v1/overviews
Response: Response:
[ [
@ -47,7 +47,7 @@ Response:
] ]
Test: Test:
curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} curl http://localhost/api/v1/overviews -v -u #{login}:#{password}
=end =end
@ -58,7 +58,7 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password}
=begin =begin
Resource: Resource:
GET /api/v1/overviews/#{id}.json GET /api/v1/overviews/#{id}
Response: Response:
{ {
@ -68,7 +68,7 @@ Response:
} }
Test: Test:
curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password} curl http://localhost/api/v1/overviews/#{id} -v -u #{login}:#{password}
=end =end
@ -79,7 +79,7 @@ curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password}
=begin =begin
Resource: Resource:
POST /api/v1/overviews.json POST /api/v1/overviews
Payload: Payload:
{ {
@ -101,7 +101,7 @@ Response:
} }
Test: Test:
curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}' curl http://localhost/api/v1/overviews -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"name": "some_name","active": true, "note": "some note"}'
=end =end
@ -112,7 +112,7 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Conte
=begin =begin
Resource: Resource:
PUT /api/v1/overviews/{id}.json PUT /api/v1/overviews/{id}
Payload: Payload:
{ {
@ -134,7 +134,7 @@ Response:
} }
Test: Test:
curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}' curl http://localhost/api/v1/overviews -v -u #{login}:#{password} -H "Content-Type: application/json" -X PUT -d '{"name": "some_name","active": true, "note": "some note"}'
=end =end
@ -145,17 +145,55 @@ curl http://localhost/api/v1/overviews.json -v -u #{login}:#{password} -H "Conte
=begin =begin
Resource: Resource:
DELETE /api/v1/overviews/{id}.json DELETE /api/v1/overviews/{id}
Response: Response:
{} {}
Test: Test:
curl http://localhost/api/v1/overviews/#{id}.json -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE curl http://localhost/api/v1/overviews/#{id} -v -u #{login}:#{password} -H "Content-Type: application/json" -X DELETE
=end =end
def destroy def destroy
model_destroy_render(Overview, params) model_destroy_render(Overview, params)
end end
=begin
Resource:
POST /api/v1/overviews_prio
Payload:
{
"prios": [
[overview_id, prio],
[overview_id, prio],
[overview_id, prio],
[overview_id, prio],
[overview_id, prio]
]
}
Response:
{
"success": true,
}
Test:
curl http://localhost/api/v1/overviews_prio -v -u #{login}:#{password} -H "Content-Type: application/json" -X POST -d '{"prios": [ [1,1], [44,2] ]}'
=end
def prio
Overview.without_callback(:update, :before, :rearrangement) do
params[:prios].each do |overview_prio|
overview = Overview.find(overview_prio[0])
next if overview.prio == overview_prio[1]
overview.prio = overview_prio[1]
overview.save!
end
end
render json: { success: true }, status: :ok
end
end end

View file

@ -75,12 +75,12 @@ get assets and record_ids of selector
attribute_ref_class = models[attribute_class][:reflections][reflection].klass attribute_ref_class = models[attribute_class][:reflections][reflection].klass
if content['value'].instance_of?(Array) if content['value'].instance_of?(Array)
content['value'].each do |item_id| content['value'].each do |item_id|
attribute_object = attribute_ref_class.find_by(id: item_id) next if item_id.blank?
if attribute_object attribute_object = attribute_ref_class.lookup(id: item_id)
assets = attribute_object.assets(assets) next if !attribute_object
end assets = attribute_object.assets(assets)
end end
else elsif content['value'].present?
attribute_object = attribute_ref_class.find_by(id: content['value']) attribute_object = attribute_ref_class.find_by(id: content['value'])
if attribute_object if attribute_object
assets = attribute_object.assets(assets) assets = attribute_object.assets(assets)
@ -138,11 +138,11 @@ get assets of object list
require item['object'].to_filename require item['object'].to_filename
record = Kernel.const_get(item['object']).find(item['o_id']) record = Kernel.const_get(item['object']).find(item['o_id'])
assets = record.assets(assets) assets = record.assets(assets)
if item['created_by_id'] if item['created_by_id'].present?
user = User.find(item['created_by_id']) user = User.find(item['created_by_id'])
assets = user.assets(assets) assets = user.assets(assets)
end end
if item['updated_by_id'] if item['updated_by_id'].present?
user = User.find(item['updated_by_id']) user = User.find(item['updated_by_id'])
assets = user.assets(assets) assets = user.assets(assets)
end end

View file

@ -17,26 +17,47 @@ class Overview < ApplicationModel
validates :name, presence: true validates :name, presence: true
before_create :fill_link_on_create, :fill_prio before_create :fill_link_on_create, :fill_prio
before_update :fill_link_on_update before_update :fill_link_on_update, :rearrangement
private private
def rearrangement
return true if !changes['prio']
prio = 0
Overview.all.order(prio: :asc, updated_at: :desc).pluck(:id).each do |overview_id|
prio += 1
next if id == overview_id
Overview.without_callback(:update, :before, :rearrangement) do
overview = Overview.find(overview_id)
next if overview.prio == prio
overview.prio = prio
overview.save!
end
end
end
def fill_prio def fill_prio
return true if prio return true if prio.present?
self.prio = 9999 self.prio = Overview.count + 1
true true
end end
def fill_link_on_create def fill_link_on_create
return true if link.present? self.link = if link.present?
self.link = link_name(name) link_name(link)
else
link_name(name)
end
true true
end end
def fill_link_on_update def fill_link_on_update
return true if !changes['name'] return true if !changes['name'] && !changes['link']
return true if changes['link'] self.link = if link.present?
self.link = link_name(name) link_name(link)
else
link_name(name)
end
true true
end end
@ -50,12 +71,16 @@ class Overview < ApplicationModel
local_link = id || rand(999) local_link = id || rand(999)
end end
check = true check = true
count = 0
local_lookup_link = local_link
while check while check
exists = Overview.find_by(link: local_link) count += 1
if exists&.id != id exists = Overview.find_by(link: local_lookup_link)
local_link = "#{local_link}_#{rand(999)}" if exists && exists.id != id # rubocop:disable Style/SafeNavigation
local_lookup_link = "#{local_link}_#{count}"
else else
check = false check = false
local_link = local_lookup_link
end end
end end
local_link local_link

View file

@ -40,9 +40,7 @@ returns
next if !user next if !user
data = user.assets(data) data = user.assets(data)
end end
data = assets_of_selector('condition', data) data = assets_of_selector('condition', data)
end end
%w[created_by_id updated_by_id].each do |local_user_id| %w[created_by_id updated_by_id].each do |local_user_id|
next if !self[ local_user_id ] next if !self[ local_user_id ]

View file

@ -7,5 +7,6 @@ Zammad::Application.routes.draw do
match api_path + '/overviews', to: 'overviews#create', via: :post match api_path + '/overviews', to: 'overviews#create', via: :post
match api_path + '/overviews/:id', to: 'overviews#update', via: :put match api_path + '/overviews/:id', to: 'overviews#update', via: :put
match api_path + '/overviews/:id', to: 'overviews#destroy', via: :delete match api_path + '/overviews/:id', to: 'overviews#destroy', via: :delete
match api_path + '/overviews_prio', to: 'overviews#prio', via: :post
end end

View file

@ -10,7 +10,8 @@ fill your database with demo records
customers: 1000, customers: 1000,
groups: 20, groups: 20,
organizations: 40, organizations: 40,
tickets: 100 overviews: 5,
tickets: 100,
) )
or if you only want to create 100 tickets or if you only want to create 100 tickets
@ -25,6 +26,7 @@ or if you only want to create 100 tickets
customers = params[:customers] || 0 customers = params[:customers] || 0
groups = params[:groups] || 0 groups = params[:groups] || 0
organizations = params[:organizations] || 0 organizations = params[:organizations] || 0
overviews = params[:overviews] || 0
tickets = params[:tickets] || 0 tickets = params[:tickets] || 0
puts 'load db with:' puts 'load db with:'
@ -32,6 +34,7 @@ or if you only want to create 100 tickets
puts " customers:#{customers}" puts " customers:#{customers}"
puts " groups:#{groups}" puts " groups:#{groups}"
puts " organizations:#{organizations}" puts " organizations:#{organizations}"
puts " overviews:#{overviews}"
puts " tickets:#{tickets}" puts " tickets:#{tickets}"
# set current user # set current user
@ -39,7 +42,7 @@ or if you only want to create 100 tickets
# organizations # organizations
organization_pool = [] organization_pool = []
if organizations && !organizations.zero? if !organizations.zero?
(1..organizations).each do (1..organizations).each do
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
organization = Organization.create!(name: "FillOrganization::#{rand(999_999)}", active: true) organization = Organization.create!(name: "FillOrganization::#{rand(999_999)}", active: true)
@ -53,7 +56,7 @@ or if you only want to create 100 tickets
# create agents # create agents
agent_pool = [] agent_pool = []
if agents && !agents.zero? if !agents.zero?
roles = Role.where(name: [ 'Agent']) roles = Role.where(name: [ 'Agent'])
groups_all = Group.all groups_all = Group.all
@ -81,7 +84,7 @@ or if you only want to create 100 tickets
# create customer # create customer
customer_pool = [] customer_pool = []
if customers && !customers.zero? if !customers.zero?
roles = Role.where(name: [ 'Customer']) roles = Role.where(name: [ 'Customer'])
groups_all = Group.all groups_all = Group.all
@ -113,7 +116,7 @@ or if you only want to create 100 tickets
# create groups # create groups
group_pool = [] group_pool = []
if groups && !groups.zero? if !groups.zero?
(1..groups).each do (1..groups).each do
ActiveRecord::Base.transaction do ActiveRecord::Base.transaction do
@ -133,6 +136,35 @@ or if you only want to create 100 tickets
puts " take #{group_pool.length} groups" puts " take #{group_pool.length} groups"
end end
# create overviews
if !overviews.zero?
(1..overviews).each do
ActiveRecord::Base.transaction do
overview = Overview.create!(
name: "Filloverview::#{rand(999_999)}",
role_ids: [Role.find_by(name: 'Agent').id],
condition: {
'ticket.state_id' => {
operator: 'is',
value: Ticket::State.by_category(:work_on_all).pluck(:id),
},
},
order: {
by: 'created_at',
direction: 'ASC',
},
view: {
d: %w[title customer group state owner created_at],
s: %w[title customer group state owner created_at],
m: %w[number title customer group state owner created_at],
view_mode_default: 's',
},
active: true
)
end
end
end
# create tickets # create tickets
priority_pool = Ticket::Priority.all priority_pool = Ticket::Priority.all
state_pool = Ticket::State.all state_pool = Ticket::State.all

View file

@ -0,0 +1,179 @@
require 'test_helper'
class OverviewsControllerTest < ActionDispatch::IntegrationTest
setup do
# set accept header
@headers = { 'ACCEPT' => 'application/json', 'CONTENT_TYPE' => 'application/json' }
# create agent
roles = Role.where(name: %w[Admin Agent])
groups = Group.all
UserInfo.current_user_id = 1
@admin = User.create_or_update(
login: 'tickets-admin',
firstname: 'Tickets',
lastname: 'Admin',
email: 'tickets-admin@example.com',
password: 'adminpw',
active: true,
roles: roles,
groups: groups,
)
# create agent
roles = Role.where(name: 'Agent')
@agent = User.create_or_update(
login: 'tickets-agent@example.com',
firstname: 'Tickets',
lastname: 'Agent',
email: 'tickets-agent@example.com',
password: 'agentpw',
active: true,
roles: roles,
groups: Group.all,
)
end
test 'no permissions' do
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-agent', 'agentpw')
params = {
name: 'Overview2',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
}
post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(401)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('authentication failed', result['error'])
end
test 'create overviews' do
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin', 'adminpw')
params = {
name: 'Overview2',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
}
post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(201)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('Overview2', result['name'])
assert_equal('my_overview', result['link'])
post '/api/v1/overviews', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(201)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal('Overview2', result['name'])
assert_equal('my_overview_1', result['link'])
end
test 'set mass prio' do
overview1 = Overview.create!(
name: 'Overview1',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
prio: 1,
updated_by_id: 1,
created_by_id: 1,
)
overview2 = Overview.create!(
name: 'Overview2',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
prio: 2,
updated_by_id: 1,
created_by_id: 1,
)
credentials = ActionController::HttpAuthentication::Basic.encode_credentials('tickets-admin', 'adminpw')
params = {
prios: [
[overview2.id, 1],
[overview1.id, 2],
]
}
post '/api/v1/overviews_prio', params: params.to_json, headers: @headers.merge('Authorization' => credentials)
assert_response(200)
result = JSON.parse(@response.body)
assert_equal(Hash, result.class)
assert_equal(true, result['success'])
overview1.reload
overview2.reload
assert_equal(2, overview1.prio)
assert_equal(1, overview2.prio)
end
end

View file

@ -28,7 +28,7 @@ class OverviewTest < ActiveSupport::TestCase
overview.destroy! overview.destroy!
overview = Overview.create!( overview = Overview.create!(
name: 'My assigned Tickets', name: 'My assigned Tickets 2',
condition: { condition: {
'ticket.state_id' => { 'ticket.state_id' => {
operator: 'is', operator: 'is',
@ -46,7 +46,7 @@ class OverviewTest < ActiveSupport::TestCase
view_mode_default: 's', view_mode_default: 's',
}, },
) )
assert_equal(overview.link, 'my_assigned_tickets') assert_equal(overview.link, 'my_assigned_tickets_2')
overview.destroy! overview.destroy!
overview = Overview.create!( overview = Overview.create!(
@ -210,6 +210,158 @@ class OverviewTest < ActiveSupport::TestCase
assert_equal(overview.link, 'my_overview2') assert_equal(overview.link, 'my_overview2')
overview.destroy! overview.destroy!
end
test 'same url' do
UserInfo.current_user_id = 1
overview1 = Overview.create!(
name: 'My own assigned Tickets',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
)
assert_equal(overview1.link, 'my_own_assigned_tickets')
overview2 = Overview.create!(
name: 'My own assigned Tickets',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
)
assert_equal(overview2.link, 'my_own_assigned_tickets_1')
overview3 = Overview.create!(
name: 'My own assigned Tickets',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
)
assert_equal(overview3.link, 'my_own_assigned_tickets_2')
overview1.destroy!
overview2.destroy!
overview3.destroy!
end
test 'priority rearrangement' do
UserInfo.current_user_id = 1
overview1 = Overview.create!(
name: 'Overview1',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
prio: 1,
)
overview2 = Overview.create!(
name: 'Overview2',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
prio: 2,
)
overview3 = Overview.create!(
name: 'Overview3',
link: 'my_overview',
condition: {
'ticket.state_id' => {
operator: 'is',
value: [1, 2, 3],
},
},
order: {
by: 'created_at',
direction: 'DESC',
},
view: {
d: %w[title customer state created_at],
s: %w[number title customer state created_at],
m: %w[number title customer state created_at],
view_mode_default: 's',
},
prio: 3,
)
overview2.prio = 3
overview2.save!
overviews = Overview.all.order(prio: :asc).pluck(:id)
assert_equal(overview1.id, overviews[0])
assert_equal(overview3.id, overviews[1])
assert_equal(overview2.id, overviews[2])
overview1.destroy!
overview2.destroy!
overview3.destroy!
end end
end end