diff --git a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee index 60e0a0a4b..fbcd54871 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_generic.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_generic.coffee @@ -138,7 +138,7 @@ class App.ControllerGenericIndex extends App.Controller @render() # fetch all - if !@disableInitFetch + if !@disableInitFetch && !@pageData.pagerAjax App[ @genericObject ].fetchFull( -> clear: true @@ -156,12 +156,45 @@ class App.ControllerGenericIndex extends App.Controller if @subscribeId App[ @genericObject ].unsubscribe(@subscribeId) + paginate: (page) => + return if page is @pageData.pagerSelected + @pageData.pagerSelected = page + @render() + render: => + if @pageData.pagerAjax + sortBy = @table?.customOrderBy || @table?.orderBy || @defaultSortBy || 'id' + orderBy = @table?.customOrderDirection || @table?.orderDirection || @defaultOrder || 'ASC' + + fallbackSortBy = sortBy + fallbackOrderBy = orderBy + if sortBy isnt 'id' + fallbackSortBy = "#{sortBy}, id" + fallbackOrderBy = "#{orderBy}, ASC" + + @startLoading() + App[@genericObject].indexFull( + (collection, data) => + @pageData.pagerTotalCount = data.total_count + @stopLoading() + @renderObjects(collection) + { + refresh: false + sort_by: fallbackSortBy + order_by: fallbackOrderBy + page: @pageData.pagerSelected + per_page: @pageData.pagerPerPage + } + ) + return objects = App[@genericObject].search( sortBy: @defaultSortBy || 'name' order: @defaultOrder ) + @renderObjects(objects) + + renderObjects: (objects) => # remove ignored items from collection if @ignoreObjectIDs @@ -208,10 +241,24 @@ class App.ControllerGenericIndex extends App.Controller }, @pageData.tableExtend ) + + if @pageData.pagerAjax + params = _.extend( + { + pagerAjax: @pageData.pagerAjax + pagerBaseUrl: @pageData.pagerBaseUrl + pagerSelected: @pageData.pagerSelected + pagerPerPage: @pageData.pagerPerPage + pagerTotalCount: @pageData.pagerTotalCount + sortRenderCallback: @render + }, + params + ) + if !@table @table = new App.ControllerTable(params) else - @table.update(objects: objects) + @table.update(objects: objects, pagerSelected: @pageData.pagerSelected, pagerTotalCount: @pageData.pagerTotalCount) edit: (id, e) => e.preventDefault() diff --git a/app/assets/javascripts/app/controllers/_application_controller_table.coffee b/app/assets/javascripts/app/controllers/_application_controller_table.coffee index d21aff4a3..413634fbf 100644 --- a/app/assets/javascripts/app/controllers/_application_controller_table.coffee +++ b/app/assets/javascripts/app/controllers/_application_controller_table.coffee @@ -196,6 +196,29 @@ class App.ControllerTable extends App.Controller App.QueueManager.run('tableRender') renderPager: (el, find = false) => + if @pagerAjax + @renderPagerAjax(el, find) + else + @renderPagerStatic(el, find) + + renderPagerAjax: (el, find = false) => + pages = parseInt((@pagerTotalCount - 1) / @pagerPerPage) + if pages < 1 + if find + el.find('.js-pager').html('') + else + el.filter('.js-pager').html('') + return + pager = App.view('generic/table_pager')( + page: @pagerSelected - 1 + pages: pages + ) + if find + el.find('.js-pager').html(pager) + else + el.filter('.js-pager').html(pager) + + renderPagerStatic: (el, find = false) => pages = parseInt(((@objects.length - 1) / @shownPerPage)) if pages < 1 if find @@ -214,6 +237,11 @@ class App.ControllerTable extends App.Controller render: => @setMaxPage() + + # always render pager in case of ajax pagination + if @pagerTotalCount + @renderPager(@el, true) + if @renderState is undefined # check if table is empty @@ -451,11 +479,14 @@ class App.ControllerTable extends App.Controller container.delegate('.js-page', 'click', (e) => e.stopPropagation() page = $(e.currentTarget).attr 'data-page' - render = => - @shownPage = page - @renderTableFull() - App.QueueManager.add('tableRender', render) - App.QueueManager.run('tableRender') + if @pagerAjax + @navigate "#{@pagerBaseUrl}#{(parseInt(page) + 1)}" + else + render = => + @shownPage = page + @renderTableFull() + App.QueueManager.add('tableRender', render) + App.QueueManager.run('tableRender') ) @el.html(container) @@ -660,6 +691,19 @@ class App.ControllerTable extends App.Controller @lastOrderDirection = orderDirection @lastOrderBy = orderBy + # sorting for ajax pagination will be made in the backend + # so we only set the arrow for the sort direction + if @pagerAjax + for header in @headers + if header.name is orderBy || "#{header.name}_id" is orderBy || header.name is "#{orderBy}_id" + if orderDirection is 'DESC' + header.sortOrderIcon = ['arrow-down', 'table-sort-arrow'] + else + header.sortOrderIcon = ['arrow-up', 'table-sort-arrow'] + else + header.sortOrderIcon = undefined + return + # Underscore's sortBy cannot deal with null values, so we replace null values with a place holder string sortBy = (list, iteratee) -> _.sortBy( @@ -964,6 +1008,10 @@ class App.ControllerTable extends App.Controller sortByColumn: (event) => column = $(event.currentTarget).closest('[data-column-key]').attr('data-column-key') + # for ajax pagination we only accept valid attributes for sorting + if @model && @pagerAjax + return if !@attributesList[column] + orderBy = @customOrderBy || @orderBy orderDirection = @customOrderDirection || @orderDirection @@ -988,6 +1036,10 @@ class App.ControllerTable extends App.Controller render = => @renderTableFull(false, skipHeadersResize: true) App.QueueManager.add('tableRender', render) + + if @sortRenderCallback + App.QueueManager.add('tableRender', @sortRenderCallback) + App.QueueManager.run('tableRender') preferencesStore: (type, key, value) -> diff --git a/app/assets/javascripts/app/controllers/groups.coffee b/app/assets/javascripts/app/controllers/groups.coffee index 2feff9b40..74e5f3708 100644 --- a/app/assets/javascripts/app/controllers/groups.coffee +++ b/app/assets/javascripts/app/controllers/groups.coffee @@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent el: @el id: @id genericObject: 'Group' + defaultSortBy: 'name' pageData: home: 'groups' object: 'Group' objects: 'Groups' + pagerAjax: true + pagerBaseUrl: '#manage/groups/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#groups' notes: [ 'Groups are ...' @@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('Group', { prio: 1500, name: 'Groups', parent: '#manage', target: '#manage/groups', controller: Index, permission: ['admin.group'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/job.coffee b/app/assets/javascripts/app/controllers/job.coffee index c4408600f..49c379820 100644 --- a/app/assets/javascripts/app/controllers/job.coffee +++ b/app/assets/javascripts/app/controllers/job.coffee @@ -13,6 +13,10 @@ class Index extends App.ControllerSubContent home: 'Jobs' object: 'Scheduler' objects: 'Schedulers' + pagerAjax: true + pagerBaseUrl: '#manage/job/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#Jobs' notes: [ 'Scheduler are ...' @@ -24,4 +28,11 @@ class Index extends App.ControllerSubContent large: true ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('Job', { prio: 3400, name: 'Scheduler', parent: '#manage', target: '#manage/job', controller: Index, permission: ['admin.scheduler'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/macro.coffee b/app/assets/javascripts/app/controllers/macro.coffee index 8c326973a..801db2459 100644 --- a/app/assets/javascripts/app/controllers/macro.coffee +++ b/app/assets/javascripts/app/controllers/macro.coffee @@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent el: @el id: @id genericObject: 'Macro' + defaultSortBy: 'name' pageData: home: 'macros' object: 'Macro' objects: 'Macros' + pagerAjax: true + pagerBaseUrl: '#manage/macros/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#macros' notes: [ 'Text modules are ...' @@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('Macros', { prio: 2310, name: 'Macros', parent: '#manage', target: '#manage/macros', controller: Index, permission: ['admin.macro'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/manage.coffee b/app/assets/javascripts/app/controllers/manage.coffee index 417bbe8d9..2d296088e 100644 --- a/app/assets/javascripts/app/controllers/manage.coffee +++ b/app/assets/javascripts/app/controllers/manage.coffee @@ -21,6 +21,7 @@ class ManageRouter extends App.ControllerPermanent App.Config.set('manage', ManageRouter, 'Routes') App.Config.set('manage/:target', ManageRouter, 'Routes') +App.Config.set('manage/:target/:page', ManageRouter, 'Routes') App.Config.set('settings/:target', ManageRouter, 'Routes') App.Config.set('channels/:target', ManageRouter, 'Routes') App.Config.set('channels/:target/:channel_id', ManageRouter, 'Routes') diff --git a/app/assets/javascripts/app/controllers/organizations.coffee b/app/assets/javascripts/app/controllers/organizations.coffee index 07efb1a1d..05a20ecb3 100644 --- a/app/assets/javascripts/app/controllers/organizations.coffee +++ b/app/assets/javascripts/app/controllers/organizations.coffee @@ -13,10 +13,15 @@ class Index extends App.ControllerSubContent baseUrl: '/api/v1/organizations' container: @el.closest('.content') ) + defaultSortBy: 'name' pageData: home: 'organizations' object: 'Organization' objects: 'Organizations' + pagerAjax: true + pagerBaseUrl: '#manage/organizations/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#organizations' notes: [ 'Organizations are for any person in the system. Agents (Owners, Resposbiles, ...) and Customers.' @@ -28,4 +33,12 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + + App.Config.set('Organization', { prio: 2000, name: 'Organizations', parent: '#manage', target: '#manage/organizations', controller: Index, permission: ['admin.organization'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/report_profile.coffee b/app/assets/javascripts/app/controllers/report_profile.coffee index 71f1f6892..865a4d709 100644 --- a/app/assets/javascripts/app/controllers/report_profile.coffee +++ b/app/assets/javascripts/app/controllers/report_profile.coffee @@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent el: @el id: @id genericObject: 'ReportProfile' + defaultSortBy: 'name' pageData: home: 'report_profiles' object: 'Report Profile' objects: 'Report Profiles' + pagerAjax: true + pagerBaseUrl: '#manage/report_profiles/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#report_profiles' notes: [ # 'Report Profile are ...' @@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('ReportProfile', { prio: 8000, name: 'Report Profiles', parent: '#manage', target: '#manage/report_profiles', controller: Index, permission: ['admin.report_profile'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/role.coffee b/app/assets/javascripts/app/controllers/role.coffee index a274843ce..dfa2616a4 100644 --- a/app/assets/javascripts/app/controllers/role.coffee +++ b/app/assets/javascripts/app/controllers/role.coffee @@ -8,10 +8,15 @@ class Index extends App.ControllerSubContent el: @el id: @id genericObject: 'Role' + defaultSortBy: 'name' pageData: home: 'roles' object: 'Role' objects: 'Roles' + pagerAjax: true + pagerBaseUrl: '#manage/roles/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#roles' notes: [ 'Roles are ...' @@ -22,4 +27,11 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('Role', { prio: 1600, name: 'Roles', parent: '#manage', target: '#manage/roles', controller: Index, permission: ['admin.role'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/text_module.coffee b/app/assets/javascripts/app/controllers/text_module.coffee index 2aa86fa2b..2d39b1a72 100644 --- a/app/assets/javascripts/app/controllers/text_module.coffee +++ b/app/assets/javascripts/app/controllers/text_module.coffee @@ -8,6 +8,7 @@ class Index extends App.ControllerSubContent el: @el id: @id genericObject: 'TextModule' + defaultSortBy: 'name' importCallback: -> new App.Import( baseUrl: '/api/v1/text_modules' @@ -18,6 +19,10 @@ class Index extends App.ControllerSubContent home: 'text_modules' object: 'TextModule' objects: 'Text modules' + pagerAjax: true + pagerBaseUrl: '#manage/text_modules/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#text_modules' notes: [ 'Text modules are ...' @@ -29,4 +34,11 @@ class Index extends App.ControllerSubContent container: @el.closest('.content') ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('TextModule', { prio: 2300, name: 'Text modules', parent: '#manage', target: '#manage/text_modules', controller: Index, permission: ['admin.text_module'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/controllers/trigger.coffee b/app/assets/javascripts/app/controllers/trigger.coffee index bd66ebf36..202760626 100644 --- a/app/assets/javascripts/app/controllers/trigger.coffee +++ b/app/assets/javascripts/app/controllers/trigger.coffee @@ -13,6 +13,10 @@ class Index extends App.ControllerSubContent home: 'triggers' object: 'Trigger' objects: 'Triggers' + pagerAjax: true + pagerBaseUrl: '#manage/trigger/' + pagerSelected: ( @page || 1 ) + pagerPerPage: 150 navupdate: '#triggers' notes: [ 'Triggers are ...' @@ -24,4 +28,11 @@ class Index extends App.ControllerSubContent veryLarge: true ) + show: (params) => + for key, value of params + if key isnt 'el' && key isnt 'shown' && key isnt 'match' + @[key] = value + + @genericController.paginate( @page || 1 ) + App.Config.set('Trigger', { prio: 3300, name: 'Trigger', parent: '#manage', target: '#manage/trigger', controller: Index, permission: ['admin.trigger'] }, 'NavBarAdmin') diff --git a/app/assets/javascripts/app/models/_application_model.coffee b/app/assets/javascripts/app/models/_application_model.coffee index 5a9771b14..2b2136d25 100644 --- a/app/assets/javascripts/app/models/_application_model.coffee +++ b/app/assets/javascripts/app/models/_application_model.coffee @@ -621,6 +621,74 @@ set new attributes of model (remove already available attributes) App.Log.error('Model', statusText, error, url) ) + ### + + index full collection (with assets) + + App.Model.indexFull(@callback) + + App.Model.indexFull( + @callback + page: 1 + per_page: 10 + sort_by: 'name' + order_by: 'ASC' + ) + + + ### + @indexFull: (callback, params = {}) -> + url = "#{@url}?full=true" + for key in ['page', 'per_page', 'sort_by', 'order_by'] + continue if !params[key] + url += "&#{key}=#{params[key]}" + + App.Log.debug('Model', "indexFull collection #{@className}", url) + + # request already active, queue callback + queueManagerName = "#{@className}::indexFull" + + if params.refresh is undefined + params.refresh = true + + App.Ajax.request( + type: 'GET' + url: url + processData: true, + success: (data, status, xhr) => + App.Log.debug('Model', "got indexFull collection #{@className}", data) + + recordIds = data.record_ids + if data.record_ids is undefined + recordIds = data[ @className.toLowerCase() + '_ids' ] + + # full / load assets + if data.assets + App.Collection.loadAssets(data.assets, targetModel: @className) + + # if no record_ids are found, no initial render is fired + if data.record_ids && _.isEmpty(data.record_ids) && params.refresh + App[@className].trigger('refresh', []) + + # find / load object + else if params.refresh + App[@className].refresh(data) + + if callback + localCallback = => + collection = [] + for id in recordIds + collection.push App[@className].find(id) + callback(collection, data) + App.QueueManager.add(queueManagerName, localCallback) + + App.QueueManager.run(queueManagerName) + + error: (xhr, statusText, error) => + @indexFullActive = false + App.Log.error('Model', statusText, error, url) + ) + @_bindsEmpty: -> if @SUBSCRIPTION_ITEM for id, keys of @SUBSCRIPTION_ITEM @@ -667,8 +735,9 @@ set new attributes of model (remove already available attributes) # just show this values in result, all filters need to match to get shown filter: - some_attribute1: ['only_this_value1', 'only_that_value1'] - some_attribute2: ['only_this_value2', 'only_that_value2'] + + # check single value + some_attribute1: 'only_this_value1' # just show this values in result, all filters need to match to get shown filterExtended: diff --git a/app/controllers/application_controller/renders_models.rb b/app/controllers/application_controller/renders_models.rb index 9129de0c5..b489966f9 100644 --- a/app/controllers/application_controller/renders_models.rb +++ b/app/controllers/application_controller/renders_models.rb @@ -105,7 +105,12 @@ module ApplicationController::RendersModels per_page = (params[:per_page] || 500).to_i offset = (page - 1) * per_page - generic_objects = object.order(id: :asc).offset(offset).limit(per_page) + sql_helper = ::SqlHelper.new(object: object) + sort_by = sql_helper.get_sort_by(params, 'id') + order_by = sql_helper.get_order_by(params, 'ASC') + order_sql = sql_helper.get_order(sort_by, order_by) + + generic_objects = object.order(Arel.sql(order_sql)).offset(offset).limit(per_page) if response_expand? list = [] @@ -124,8 +129,9 @@ module ApplicationController::RendersModels assets = item.assets(assets) end render json: { - record_ids: item_ids, - assets: assets, + record_ids: item_ids, + assets: assets, + total_count: object.count }, status: :ok return end diff --git a/app/controllers/organizations_controller.rb b/app/controllers/organizations_controller.rb index 2474bc0d0..d94687946 100644 --- a/app/controllers/organizations_controller.rb +++ b/app/controllers/organizations_controller.rb @@ -48,48 +48,7 @@ curl http://localhost/api/v1/organizations -v -u #{login}:#{password} =end def index - offset = 0 - per_page = 500 - - if params[:page] && params[:per_page] - offset = (params[:page].to_i - 1) * params[:per_page].to_i - per_page = params[:per_page].to_i - end - - if per_page > 500 - per_page = 500 - end - - organizations = policy_scope(Organization).order(id: :asc).offset(offset).limit(per_page) - - if response_expand? - list = [] - organizations.each do |organization| - list.push organization.attributes_with_association_names - end - render json: list, status: :ok - return - end - - if response_full? - assets = {} - item_ids = [] - organizations.each do |item| - item_ids.push item.id - assets = item.assets(assets) - end - render json: { - record_ids: item_ids, - assets: assets, - }, status: :ok - return - end - - list = [] - organizations.each do |organization| - list.push organization.attributes_with_association_ids - end - render json: list + model_index_render(policy_scope(Organization), params) end =begin diff --git a/app/models/concerns/has_search_sortable.rb b/app/models/concerns/has_search_sortable.rb deleted file mode 100644 index 75e239db1..000000000 --- a/app/models/concerns/has_search_sortable.rb +++ /dev/null @@ -1,166 +0,0 @@ -# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ -module HasSearchSortable - extend ActiveSupport::Concern - - # methods defined here are going to extend the class, not the instance of it - class_methods do - -=begin - -This function will check the params for the "sort_by" attribute -and validate its values. - - sort_by = search_get_sort_by(params, default) - -returns - - sort_by = [ - 'created_at', - 'updated_at', - ] - -=end - - def search_get_sort_by(params, default) - sort_by = [] - if params[:sort_by].present? && params[:sort_by].is_a?(String) - params[:sort_by] = [params[:sort_by]] - elsif params[:sort_by].blank? - params[:sort_by] = [] - end - - # check order - params[:sort_by].each do |value| - - # only accept values which are set for the db schema - raise "Found invalid column '#{value}' for sorting." if columns_hash[value].blank? - - sort_by.push(value) - end - - if sort_by.blank? - if default.is_a?(Array) - sort_by = default - else - sort_by.push(default) - end - end - - sort_by - end - -=begin - -This function will check the params for the "order_by" attribute -and validate its values. - - order_by = search_get_order_by(params, default) - -returns - - order_by = [ - 'asc', - 'desc', - ] - -=end - - def search_get_order_by(params, default) - order_by = [] - if params[:order_by].present? && params[:order_by].is_a?(String) - params[:order_by] = [ params[:order_by] ] - elsif params[:order_by].blank? - params[:order_by] = [] - end - - # check order - params[:order_by].each do |value| - raise "Found invalid order by value #{value}. Please use 'asc' or 'desc'." if !value.match?(/\A(asc|desc)\z/i) - - order_by.push(value.downcase) - end - - if order_by.blank? - if default.is_a?(Array) - order_by = default - else - order_by.push(default) - end - end - - order_by - end - -=begin - -This function will use the evaluated values for sort_by and -order_by to generate the ORDER-SELECT sql statement for the sorting -of the result. - - sort_by = [ 'created_at', 'updated_at' ] - order_by = [ 'asc', 'desc' ] - default = 'tickets.created_at' - - sql = search_get_order_select_sql(sort_by, order_by, default) - -returns - - sql = 'tickets.created_at, tickets.updated_at' - -=end - - def search_get_order_select_sql(sort_by, order_by, default) - sql = [] - - sort_by.each_with_index do |value, index| - next if value.blank? - next if order_by[index].blank? - - sql.push( "#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)}" ) - end - - if sql.blank? - sql.push("#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}") - end - - sql.join(', ') - end - -=begin - -This function will use the evaluated values for sort_by and -order_by to generate the ORDER- sql statement for the sorting -of the result. - - sort_by = [ 'created_at', 'updated_at' ] - order_by = [ 'asc', 'desc' ] - default = 'tickets.created_at DESC' - - sql = search_get_order_sql(sort_by, order_by, default) - -returns - - sql = 'tickets.created_at ASC, tickets.updated_at DESC' - -=end - - def search_get_order_sql(sort_by, order_by, default) - sql = [] - - sort_by.each_with_index do |value, index| - next if value.blank? - next if order_by[index].blank? - - sql.push( "#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)} #{order_by[index]}" ) - end - - if sql.blank? - sql.push("#{ActiveRecord::Base.connection.quote_table_name(table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}") - end - - sql.join(', ') - end - - end - -end diff --git a/app/models/knowledge_base/search.rb b/app/models/knowledge_base/search.rb index 3299a116f..f6c82e267 100644 --- a/app/models/knowledge_base/search.rb +++ b/app/models/knowledge_base/search.rb @@ -2,21 +2,19 @@ class KnowledgeBase module Search extend ActiveSupport::Concern - included do - include HasSearchSortable - end - class_methods do def search(params) current_user = params[:current_user] # enable search only for agents and admins return [] if !search_preferences(current_user) + sql_helper = ::SqlHelper.new(object: self) + options = { limit: params[:limit] || 10, from: params[:offset] || 0, - sort_by: search_get_sort_by(params, 'updated_at'), - order_by: search_get_order_by(params, 'desc'), + sort_by: sql_helper.get_sort_by(params, 'updated_at'), + order_by: sql_helper.get_order_by(params, 'desc'), user: current_user } @@ -41,8 +39,9 @@ class KnowledgeBase end def search_sql(query, kb_locales, options) - table_name = arel_table.name - order_sql = search_get_order_sql(options[:sort_by], options[:order_by], "#{table_name}.updated_at ASC") + table_name = arel_table.name + sql_helper = ::SqlHelper.new(object: self) + order_sql = sql_helper.get_order(options[:sort_by], options[:order_by], "#{table_name}.updated_at ASC") # - stip out * we already search for *query* - query.delete! '*' diff --git a/app/models/organization/search.rb b/app/models/organization/search.rb index ef952889d..b607b3168 100644 --- a/app/models/organization/search.rb +++ b/app/models/organization/search.rb @@ -4,10 +4,6 @@ class Organization module Search extend ActiveSupport::Concern - included do - include HasSearchSortable - end - # methods defined here are going to extend the class, not the instance of it class_methods do @@ -72,11 +68,13 @@ returns offset = params[:offset] || 0 current_user = params[:current_user] + sql_helper = ::SqlHelper.new(object: self) + # check sort - positions related to order by - sort_by = search_get_sort_by(params, %w[active updated_at]) + sort_by = sql_helper.get_sort_by(params, %w[active updated_at]) # check order - positions related to sort by - order_by = search_get_order_by(params, %w[desc desc]) + order_by = sql_helper.get_order_by(params, %w[desc desc]) # enable search only for agents and admins return [] if !search_preferences(current_user) @@ -97,8 +95,8 @@ returns return organizations end - order_select_sql = search_get_order_select_sql(sort_by, order_by, 'organizations.updated_at') - order_sql = search_get_order_sql(sort_by, order_by, 'organizations.updated_at ASC') + order_select_sql = sql_helper.get_order_select(sort_by, order_by, 'organizations.updated_at') + order_sql = sql_helper.get_order(sort_by, order_by, 'organizations.updated_at ASC') # fallback do sql query # - stip out * we already search for *query* - diff --git a/app/models/ticket/search.rb b/app/models/ticket/search.rb index 8ab811ff2..64496ba2f 100644 --- a/app/models/ticket/search.rb +++ b/app/models/ticket/search.rb @@ -2,10 +2,6 @@ module Ticket::Search extend ActiveSupport::Concern - included do - include HasSearchSortable - end - # methods defined here are going to extend the class, not the instance of it class_methods do @@ -119,11 +115,13 @@ returns full = true end + sql_helper = ::SqlHelper.new(object: self) + # check sort - sort_by = search_get_sort_by(params, 'updated_at') + sort_by = sql_helper.get_sort_by(params, 'updated_at') # check order - order_by = search_get_order_by(params, 'desc') + order_by = sql_helper.get_order_by(params, 'desc') # try search index backend if condition.blank? && SearchIndexBackend.enabled? @@ -197,8 +195,8 @@ returns # do query # - stip out * we already search for *query* - - order_select_sql = search_get_order_select_sql(sort_by, order_by, 'tickets.updated_at') - order_sql = search_get_order_sql(sort_by, order_by, 'tickets.updated_at DESC') + order_select_sql = sql_helper.get_order_select(sort_by, order_by, 'tickets.updated_at') + order_sql = sql_helper.get_order(sort_by, order_by, 'tickets.updated_at DESC') if query query.delete! '*' tickets_all = Ticket.select("DISTINCT(tickets.id), #{order_select_sql}") diff --git a/app/models/user/search.rb b/app/models/user/search.rb index 76a752716..d2988423e 100644 --- a/app/models/user/search.rb +++ b/app/models/user/search.rb @@ -4,10 +4,6 @@ class User module Search extend ActiveSupport::Concern - included do - include HasSearchSortable - end - # methods defined here are going to extend the class, not the instance of it class_methods do @@ -83,11 +79,13 @@ returns offset = params[:offset] || 0 current_user = params[:current_user] + sql_helper = ::SqlHelper.new(object: self) + # check sort - positions related to order by - sort_by = search_get_sort_by(params, %w[active updated_at]) + sort_by = sql_helper.get_sort_by(params, %w[active updated_at]) # check order - positions related to sort by - order_by = search_get_order_by(params, %w[desc desc]) + order_by = sql_helper.get_order_by(params, %w[desc desc]) # enable search only for agents and admins return [] if !search_preferences(current_user) @@ -129,7 +127,7 @@ returns return users end - order_sql = search_get_order_sql(sort_by, order_by, 'users.updated_at DESC') + order_sql = sql_helper.get_order(sort_by, order_by, 'users.updated_at DESC') # fallback do sql query # - stip out * we already search for *query* - diff --git a/lib/sql_helper.rb b/lib/sql_helper.rb new file mode 100644 index 000000000..aac91e318 --- /dev/null +++ b/lib/sql_helper.rb @@ -0,0 +1,172 @@ +# Copyright (C) 2012-2016 Zammad Foundation, http://zammad-foundation.org/ + +class SqlHelper + + def initialize(object:) + @object = object + end + + def get_param_key(key, params) + sort_by = [] + if params[key].present? && params[key].is_a?(String) + params[key] = params[key].split(/\s*,\s*/) + elsif params[key].blank? + params[key] = [] + end + + sort_by + end + +=begin + +This function will check the params for the "sort_by" attribute +and validate its values. + + sql_helper = SqlHelper.new(object: Ticket) + sort_by = sql_helper.get_sort_by(params, default) + +returns + + sort_by = [ + 'created_at', + 'updated_at', + ] + +=end + + def get_sort_by(params, default = nil) + sort_by = get_param_key(:sort_by, params) + + # check order + params[:sort_by].each do |value| + + # only accept values which are set for the db schema + raise "Found invalid column '#{value}' for sorting." if @object.columns_hash[value].blank? + + sort_by.push(value) + end + + if sort_by.blank? && default.present? + if default.is_a?(Array) + sort_by = default + else + sort_by.push(default) + end + end + + sort_by + end + +=begin + +This function will check the params for the "order_by" attribute +and validate its values. + +sql_helper = SqlHelper.new(object: Ticket) +order_by = sql_helper.get_order_by(params, default) + +returns + +order_by = [ + 'asc', + 'desc', +] + +=end + + def get_order_by(params, default = nil) + order_by = get_param_key(:order_by, params) + + # check order + params[:order_by].each do |value| + raise "Found invalid order by value #{value}. Please use 'asc' or 'desc'." if !value.match?(/\A(asc|desc)\z/i) + + order_by.push(value.downcase) + end + + if order_by.blank? && default.present? + if default.is_a?(Array) + order_by = default + else + order_by.push(default) + end + end + + order_by + end + + def set_sql_order_default(sql, default) + if sql.blank? && default.present? + sql.push("#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(default)}") + end + sql + end + +=begin + +This function will use the evaluated values for sort_by and +order_by to generate the ORDER-SELECT sql statement for the sorting +of the result. + +sort_by = [ 'created_at', 'updated_at' ] +order_by = [ 'asc', 'desc' ] +default = 'tickets.created_at' + +sql_helper = SqlHelper.new(object: Ticket) +sql = sql_helper.get_order_select(sort_by, order_by, default) + +returns + +sql = 'tickets.created_at, tickets.updated_at' + +=end + + def get_order_select(sort_by, order_by, default = nil) + sql = [] + + sort_by.each_with_index do |value, index| + next if value.blank? + next if order_by[index].blank? + + sql.push( "#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)}" ) + end + + sql = set_sql_order_default(sql, default) + + sql.join(', ') + end + +=begin + +This function will use the evaluated values for sort_by and +order_by to generate the ORDER- sql statement for the sorting +of the result. + +sort_by = [ 'created_at', 'updated_at' ] +order_by = [ 'asc', 'desc' ] +default = 'tickets.created_at DESC' + +sql_helper = SqlHelper.new(object: Ticket) +sql = sql_helper.get_order(sort_by, order_by, default) + +returns + +sql = 'tickets.created_at ASC, tickets.updated_at DESC' + +=end + + def get_order(sort_by, order_by, default = nil) + sql = [] + + sort_by.each_with_index do |value, index| + next if value.blank? + next if order_by[index].blank? + + sql.push( "#{ActiveRecord::Base.connection.quote_table_name(@object.table_name)}.#{ActiveRecord::Base.connection.quote_column_name(value)} #{order_by[index]}" ) + end + + sql = set_sql_order_default(sql, default) + + sql.join(', ') + end +end diff --git a/spec/factories/report/profile.rb b/spec/factories/report/profile.rb new file mode 100644 index 000000000..e2246cdf6 --- /dev/null +++ b/spec/factories/report/profile.rb @@ -0,0 +1,8 @@ +FactoryBot.define do + factory :'report/profile', aliases: %i[report_profile] do + sequence(:name) { |n| "Report #{n}" } + active { true } + created_by_id { 1 } + updated_by_id { 1 } + end +end diff --git a/spec/system/examples/pagination_examples.rb b/spec/system/examples/pagination_examples.rb new file mode 100644 index 000000000..5e89c071b --- /dev/null +++ b/spec/system/examples/pagination_examples.rb @@ -0,0 +1,45 @@ +RSpec.shared_examples 'pagination' do |model:, klass:, path:, sort_by: :name| + def prepare(model) + create_list(model, 500) + end + + it 'does paginate' do + prepare(model) + visit path + refresh # more stability + expect(page).to have_css('.js-pager', wait: 10) + + class_page1 = klass.order(sort_by => :asc, id: :asc).offset(50).first + expect(page).to have_text(class_page1.name, wait: 10) + + page.first('.js-page', text: '2').click + await_empty_ajax_queue + + class_page2 = klass.order(sort_by => :asc, id: :asc).offset(175).first + expect(page).to have_text(class_page2.name, wait: 10) + + page.first('.js-page', text: '3').click + await_empty_ajax_queue + + class_page3 = klass.order(sort_by => :asc, id: :asc).offset(325).first + expect(page).to have_text(class_page3.name, wait: 10) + + page.first('.js-page', text: '4').click + await_empty_ajax_queue + + class_page4 = klass.order(sort_by => :asc, id: :asc).offset(475).first + expect(page).to have_text(class_page4.name, wait: 10) + + page.first('.js-page', text: '1').click + await_empty_ajax_queue + + page.first('.js-tableHead[data-column-key=name]').click + await_empty_ajax_queue + expect(page).to have_text(class_page1.name, wait: 10) + + page.first('.js-tableHead[data-column-key=name]').click + await_empty_ajax_queue + class_last = klass.order(sort_by => :desc).first + expect(page).to have_text(class_last.name, wait: 10) + end +end diff --git a/spec/system/manage/groups_spec.rb b/spec/system/manage/groups_spec.rb new file mode 100644 index 000000000..c5a7dbad3 --- /dev/null +++ b/spec/system/manage/groups_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Groups', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :group, klass: Group, path: 'manage/groups' + end +end diff --git a/spec/system/manage/jobs_spec.rb b/spec/system/manage/jobs_spec.rb new file mode 100644 index 000000000..b17ae106e --- /dev/null +++ b/spec/system/manage/jobs_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Job', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :job, klass: Job, path: 'manage/job' + end +end diff --git a/spec/system/manage/macros_spec.rb b/spec/system/manage/macros_spec.rb new file mode 100644 index 000000000..f08f54631 --- /dev/null +++ b/spec/system/manage/macros_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Macro', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :macro, klass: Macro, path: 'manage/macros' + end +end diff --git a/spec/system/manage/organizations_spec.rb b/spec/system/manage/organizations_spec.rb index 2d5950435..4a64b44da 100644 --- a/spec/system/manage/organizations_spec.rb +++ b/spec/system/manage/organizations_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'system/examples/pagination_examples' RSpec.describe 'Manage > Organizations', type: :system do @@ -48,4 +49,8 @@ RSpec.describe 'Manage > Organizations', type: :system do end end end + + context 'ajax pagination' do + include_examples 'pagination', model: :organization, klass: Organization, path: 'manage/organizations' + end end diff --git a/spec/system/manage/report_profiles_spec.rb b/spec/system/manage/report_profiles_spec.rb new file mode 100644 index 000000000..ea68735bf --- /dev/null +++ b/spec/system/manage/report_profiles_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Report Profiles', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :report_profile, klass: Report::Profile, path: 'manage/report_profiles' + end +end diff --git a/spec/system/manage/roles_spec.rb b/spec/system/manage/roles_spec.rb new file mode 100644 index 000000000..e1d87b8ee --- /dev/null +++ b/spec/system/manage/roles_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Role', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :role, klass: Role, path: 'manage/roles' + end +end diff --git a/spec/system/manage/text_modules_spec.rb b/spec/system/manage/text_modules_spec.rb new file mode 100644 index 000000000..7c036d162 --- /dev/null +++ b/spec/system/manage/text_modules_spec.rb @@ -0,0 +1,8 @@ +require 'rails_helper' +require 'system/examples/pagination_examples' + +RSpec.describe 'Manage > Text Module', type: :system do + context 'ajax pagination' do + include_examples 'pagination', model: :text_module, klass: TextModule, path: 'manage/text_modules' + end +end diff --git a/spec/system/manage/trigger_spec.rb b/spec/system/manage/trigger_spec.rb index 939f1c34a..3503c9618 100644 --- a/spec/system/manage/trigger_spec.rb +++ b/spec/system/manage/trigger_spec.rb @@ -1,4 +1,5 @@ require 'rails_helper' +require 'system/examples/pagination_examples' RSpec.describe 'Manage > Trigger', type: :system do @@ -57,4 +58,8 @@ RSpec.describe 'Manage > Trigger', type: :system do end end end + + context 'ajax pagination' do + include_examples 'pagination', model: :trigger, klass: Trigger, path: 'manage/trigger' + end end