ddnexus / pagy

🏆 The Best Pagination Ruby Gem 🥇
https://ddnexus.github.io/pagy
MIT License
4.6k stars 409 forks source link

Main pagination with others nested pagination by nested collections for JSON or Hash structure #393

Closed gwynolanga closed 2 years ago

gwynolanga commented 2 years ago

Hello! I have a problem with a main pagination with others nested pagination by nested collections for JSON or Hash structure. I have Check ActiveRecrod model which contains offenses_files attribute (json type):

[
  ...................................................... PREVIOUS RECORDS ......................................................

  {"file_path"=>"cuurjol/askme/app/models/like.rb",
  "offenses_count"=>3,
  "offenses"=>
   [{"rule"=>"Style/FrozenStringLiteralComment", "message"=>"Missing frozen string literal comment.", "line"=>1, "column"=>1},
    {"rule"=>"Rails/RedundantPresenceValidationOnBelongsTo", "message"=>"Remove explicit presence validation for `user`.", "line"=>7, "column"=>20},
    {"rule"=>"Rails/RedundantPresenceValidationOnBelongsTo", "message"=>"Remove explicit presence validation for `question`.", "line"=>8, "column"=>24}]},
 {"file_path"=>"cuurjol/askme/app/models/question.rb",
  "offenses_count"=>6,
  "offenses"=>
   [{"rule"=>"Style/FrozenStringLiteralComment", "message"=>"Missing frozen string literal comment.", "line"=>1, "column"=>1},
    {"rule"=>"Style/RedundantFreeze", "message"=>"Do not freeze immutable objects, as freezing them has no effect.", "line"=>2, "column"=>12},
    {"rule"=>"Style/RedundantParentheses", "message"=>"Don't use parentheses around a literal.", "line"=>2, "column"=>12},
    {"rule"=>"Layout/LineLength", "message"=>"Line is too long. [127/120]", "line"=>2, "column"=>121},
    {"rule"=>"Rails/RedundantPresenceValidationOnBelongsTo", "message"=>"Remove explicit presence validation for `user`.", "line"=>9, "column"=>20},
    {"rule"=>"Metrics/AbcSize", "message"=>"Assignment Branch Condition size for check_hashtags is too high. [<1, 21, 5> 21.61/17]", "line"=>16, "column"=>3}]},
 {"file_path"=>"cuurjol/askme/app/models/user.rb",
  "offenses_count"=>8,
  "offenses"=>
   [{"rule"=>"Style/FrozenStringLiteralComment", "message"=>"Missing frozen string literal comment.", "line"=>1, "column"=>1},
    {"rule"=>"Lint/DeprecatedOpenSSLConstant", "message"=>"Use `OpenSSL::Digest.new('SHA256')` instead of `OpenSSL::Digest::SHA256.new`.", "line"=>5, "column"=>12},
    {"rule"=>"Layout/SpaceInsideHashLiteralBraces", "message"=>"Space inside } missing.", "line"=>12, "column"=>112},
    {"rule"=>"Style/RedundantRegexpCharacterClass", "message"=>"Redundant single-element character class, `[\\h]` can be replaced with `\\h`.", "line"=>13, "column"=>94},
    {"rule"=>"Layout/LineLength", "message"=>"Line is too long. [122/120]", "line"=>13, "column"=>121},
    {"rule"=>"Style/GuardClause", "message"=>"Use a guard clause (`return unless password.present?`) instead of wrapping the code inside a conditional expression.", "line"=>36, "column"=>5},
    {"rule"=>"Style/UnpackFirst", "message"=>"Use `password_hash.unpack1('H*')` instead of `password_hash.unpack('H*')[0]`.", "line"=>47, "column"=>5},
    {"rule"=>"Rails/Blank", "message"=>"Use `if user.blank?` instead of `unless user.present?`.", "line"=>53, "column"=>16}]},
 {"file_path"=>"cuurjol/askme/config.ru",
  "offenses_count"=>1,
  "offenses"=>[{"rule"=>"Style/FrozenStringLiteralComment", "message"=>"Missing frozen string literal comment.", "line"=>1, "column"=>1}]},
 {"file_path"=>"cuurjol/askme/config/application.rb",
  "offenses_count"=>12,
  "offenses"=>
   [{"rule"=>"Style/FrozenStringLiteralComment", "message"=>"Missing frozen string literal comment.", "line"=>1, "column"=>1},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>3, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>5, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>6, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>7, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>8, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>9, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>10, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>11, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>12, "column"=>9},
    {"rule"=>"Style/StringLiterals", "message"=>"Prefer single-quoted strings when you don't need string interpolation or special symbols.", "line"=>13, "column"=>9},
    {"rule"=>"Style/SymbolArray", "message"=>"Use `%i` or `%I` for an array of symbols.", "line"=>35, "column"=>37}]},

    ...................................................... NEXT RECORDS ......................................................
 ]

The structure above is not ActiveRecord, it is just a Hash for Ruby. I want to paginate it simultaneously by file pathes (file_path Hash key) and by rules (rule Hash key), i.e to create a complicated pagination.

I did it for my application like this: image

The picture above has a pagination for the file pathes (main table) and have nested paginations by rule for each file path (nested tables). Below is the following controller code snippet:

def show
  @check = Repository::Check.find(params[:id])
  @pagy_file, @offenses_files = pagy_array(@check.offenses_files, page_param: :file)
  @offenses = @offenses_files.map { |file| pagy_array(file['offenses'], page_param: :offense) }
  authorize(@check)
end

Below is the following view code snippet:

- if @check.offenses_files.present?
            h2 = t('.list_of_files_title')
            .table-responsive.border.border-2.rounded-3.border-dark.mb-2
              table.table.table-bordered.table-sm.table-hover.border-dark.text-center.align-middle.mb-0
                thead.table-dark.border-dark
                  tr.align-middle
                    th[scope='col'] #
                    th[scope='col'] = t('.offenses_files.file_path')
                    th[scope='col'] = t('.offenses_files.offenses_count')
                tbody.border-dark
                  - @offenses_files.each_with_index do |file, i|
                    tr
                      td = link_to('#', data: { 'bs-toggle' => 'collapse', 'bs-target' => "#table-offenses-#{i}" },
                              aria: { expanded: 'false', controls: "table-offenses-#{i}" },
                              class: 'text-dark icon-toggle') do
                        i.fa-solid.fa-circle-plus.fa-2x.icon-collapsed
                        i.fa-solid.fa-circle-minus.fa-2x.icon-expanded
                      td = file['file_path']
                      td = file['offenses_count']
                    tr.collapse id="table-offenses-#{i}"
                      td[colspan='3']
                        .table-responsive.border.border-2.rounded-3.border-dark.mx-4.my-2
                          table.table.table-bordered.table-sm.table-hover.border-dark.text-center.align-middle.mb-0
                            thead.table-dark.border-dark
                              tr.align-middle
                                th[scope='col'] = t('.offenses_files.offenses.message')
                                th[scope='col'] = t('.offenses_files.offenses.rule')
                                th[scope='col'] = t('.offenses_files.offenses.line_column')
                            tbody.border-dark
                              - @offenses[i].last.each do |offense|
                                tr
                                  td = offense['message']
                                  td = offense['rule']
                                  td = "#{offense['line']}:#{offense['column']}"
                            tfoot.table-dark.border-dark
                              tr
                                td.border-bottom-0[colspan='6']
                                  .d-flex.flex-wrap.justify-content-between.align-items-center
                                    .mb-2.mb-md-0
                                      == pagy_info(@offenses[i].first)
                                    .pagination-sm
                                      == pagy_bootstrap_nav(@offenses[i].first) if @offenses[i].first.pages > 1
                tfoot.table-dark.border-dark
                  tr
                    td.border-bottom-0[colspan='3']
                      .d-flex.flex-wrap.justify-content-between.align-items-center
                        .mb-2.mb-md-0
                          == pagy_info(@pagy_file)
                        .pagination-sm
                          == pagy_bootstrap_nav(@pagy_file) if @pagy_file.pages > 1

The controller code snippet looks like OK, but when I try to flip a next page for any rule paginations (nested tables) I capture the following exception:

image

After a lot of research in debug mode, I noticed that each pagination for rules (nested tables) create a request with the following query parameters: file=5&offense=2 where file=5 is the fifth page for file pathes (main table) and offense=2 - is the second page for rules (nested tables) and an exception will be raised for those rules (nested tables) which should not have a pagination. I fixed the exception by the following controller code:

def show
  @check = Repository::Check.find(params[:id])
  @pagy_file, @offenses_files = pagy_array(@check.offenses_files, page_param: :file)
  @offenses = @offenses_files.map do |file|
    pagy_array(file['offenses'], page_param: :offense)
  rescue Pagy::OverflowError
    pagy_array(file['offenses'], page: 1, page_param: :offense)
  end
  authorize(@check)
end

The updated controller code snippet looks like OK, but when I try to flip a next page for one rule paginations (nested tables) I noticed that other rule paginations (nested tables) also flip a next page: image

Final request for the picture above looks the following: http://localhost:3000/repositories/4/checks/3?file=5&offense=2

For cuurjol/askme/config/application.rb file path example (third nested table): Expected result: rule pagination flip a next page (second) without side effects for others. Actual result: all existing rule paginations (nested tables) flip a next page (second).

Is there any way to solve my problem? Perhaps, I need to add id Hash keys for each file path and rule, but I don't know how to build a request with it as additional parameter for Pagy gem.

benkoshy commented 2 years ago

@cuurjol

I think a lot of the complexity can be avoided, by taking a different approach. If you will permit me, consider using turbo-frames. Some rough ideas - there may be mistakes in them, but I hope you can see the general idea.

def show
  @check = Repository::Check.find(params[:id])
  @pagy, @offenses_files = pagy(@check.offenses_files)
  authorize(@check)
end
<%  @offenses_files.each do |offence_file|%>
   <% turbo_frame_tag offence_file, src: offence_file_offences_path(@offence_file) do   %>
   <% end %>
<% end %>
# OffencesController
def index
  @offence_file = OffenceFile.find(params[:id])
  @pagy, @offenses = pagy(@offence.offence_files)
  # authorize(@offence.check)
end

And that's it! No complex logic on the back end. Yes, there are extra requests. If you want to avoid that, then simply eager load the records and use the same partials.

Any further questions, please feel free to ask.

ddnexus commented 2 years ago

For cuurjol/askme/config/application.rb file path example (third nested table): Expected result: rule pagination flip a next page (second) without side effects for others. Actual result: all existing rule paginations (nested tables) flip a next page (second).

You are using the same page param for all your different/independent paginations in the same request. How can they know that you mean to paginate each of them independently?

https://ddnexus.github.io/pagy/how-to#customize-the-page-param https://www.imaginarycloud.com/blog/how-to-paginate-ruby-on-rails-apps-with-pagy/ (a bit outdated but the concept is the same)

ddnexus commented 2 years ago

Closing this because it is not an issue. Please use the indicated channel to ask questions.