bogdan / datagrid

Gem to create tables grids with sortable columns and filters
MIT License
1.02k stars 115 forks source link

Multiple rows per asset #314

Closed gap777 closed 1 year ago

gap777 commented 1 year ago

Can you think of a way to configure/use datagrid to support multiple rows for each asset? I'm thinking about "subrows", each having a column structure compatible with the main row rendered for an asset, but essentially allowing the main row to be a rollup, and the subrows to be a view into the detailed data contributors. I'd want to hide the subrows with a collapser, and then show them on demand.

bogdan commented 1 year ago

Can you draw me the design you want to reach? ASCII art works.

gap777 commented 1 year ago
                  col1                            col2                                   col3                                       col4
1                asset1-button           asset1-col2 data              asset1-col3 data                  asset1-col4 data
2                blank                          blank                                 asset1-col3* data                asset1-col4* data   
3                blank                          blank                                 asset1-col3** data              asset1-col4** data   

where asset1-col3* data is a part of the rollup reflected in asset1-col3 data (as is asset1-col3** data). And in this case, rows 2 and 3 are initially hidden, and only visible via clicking asset1-button. Id handle the hiding/showing, but the question Im asking is about the rendering of more than 1 kind of row...

Maybe this could be done with just conditionally logic in the column definitions... Maybe this could be done with a way to express multiple column definitions for a single column / asset (based on some state). Maybe this could be done with injecting multiple kinds of assets...

bogdan commented 1 year ago

Here is what you can do: (Note: it requires addtional option I've implemented in master. Please try to use the gem from master directly before I release it).

class MyGrid

  class_attribute :tiers_count, default: 3

  def self.tiered_column(name, options, blocks)
    tiers_count.times do |tier|
      block = blocks[tier] || -> { '--blank--' }
      column(name, {**options, tier: index}, &block)
    end
  end

  tiered_columns(:col1, {header: "Col 1"}, [
    -> (model) { model.first_name },
  ])
  tiered_columns(:col2, {header: "Col 2"}, [
    -> (model) {model.last_name},
  ])

  tiered_columns(:col3, {header: "Col 3"}, [
    -> (model) { model.method1 },
    -> (model) { model.method2 },
    -> (model) { model.method3 },
  ])

  def columns_by_tier(tier)
    columns.select{|c| c.options[:tier] == tier}
  end

  def tiers
    0..(tiers_count - 1)
  end

end
%table
  = datagrid_header(@grid, columns: @grid.columns_by_tier(1))
  - @grid.assets.each do |asset|
    - @grid.tiers.each do |tier|
      -# HTML seems to allow multiple tbody tags within the same table
      -# I decided to use it for UI control
      %tbody.js-tier{data: {tier: tier}}
        = datagrid_row(@grid, asset, columns: @grid.columns_by_tier(tier))
fzf commented 7 months ago

I am having some difficulties getting this to work. Is this still supported?

bogdan commented 7 months ago

Show me the code you have and the error you get. I tried the following myself and it worked:

require "datagrid"

Person = Struct.new(:first_name, :last_name, :age, :job)

class TestGrid
  include Datagrid

  scope do
    [
      Person.new("John", "Smith", 25, "Developer"),
      Person.new("Alfred", "Black", 35, "Manager"),
    ]
  end

  def self.tiers
    0..(tiers_count - 1)
  end

  class_attribute :tiers_count, default: 2

  def self.tiered_columns(name, options, blocks)
    tiers.each do |tier|
      block = blocks[tier] || -> (_) { '--blank--' }
      column(name, **options, tier: tier) do |model|
        block.call(model)
      end
    end
  end

  tiered_columns(:col1, {header: "Col 1"}, [
    -> (model) { model.first_name },
  ])
  tiered_columns(:col2, {header: "Col 2"}, [
    -> (model) {model.last_name},
  ])

  tiered_columns(:col3, {header: "Col 3"}, [
    -> (model) { model.age },
    -> (model) { model.job },
    # -> (model) { model.method3 },
  ])

  def columns_by_tier(tier)
    columns.select{|c| c.options[:tier] == tier}
  end

  def tiers
    self.class.tiers
  end
end

grid = TestGrid.new

data = grid.assets.map do |asset|
  grid.tiers.map do |tier|
    [
      tier,
      *grid.columns_by_tier(tier).map do |column|
        grid.data_value(column, asset)
      end,
    ]
  end
end.flatten(1)

puts data.map {|row| row.join(',')}.join("\n")
fzf commented 7 months ago

I am trying to use this with the built in views slightly modified from your previous example. It is rendering but not as expected:

Screenshot 2024-01-13 at 10 54 00

class PurchasesGrid < BaseGrid
  scope do
    Purchase
  end

  def self.tiers
    0..(tiers_count - 1)
 end

 class_attribute :tiers_count, default: 2

 def self.tiered_columns(name, options, blocks)
    tiers.each do |tier|
      block = blocks[tier] || -> (_) { '--blank--' }
      column(name, **options, tier: tier) do |model|
        block.call(model)
      end
    end
 end

 tiered_columns(:col1, {header: "Col 1"}, [
    -> (model) { model.name },
 ])
 tiered_columns(:col2, {header: "Col 2"}, [
    -> (model) {model.amount },
    -> (model) {model.purchased_at },
 ])

 tiered_columns(:col3, {header: "Col 3"}, [
    -> (model) { model.tip },
    -> (model) { model.tip_percentage },
    # -> (model) { model.method3 },
 ])

 def columns_by_tier(tier)
    columns.select{|c| c.options[:tier] == tier}
 end

 def tiers
    self.class.tiers
 end
<div class="flex flex-col justify-center items-center">
  <% if grid.html_columns(*options[:columns]).any? %>
    <%= content_tag :table, options[:html].merge({class: "min-w-full text-left text-sm font-light"}) do %>
      <thead class="border-b font-medium dark:border-neutral-500">
        <%= datagrid_header(grid, options) %>
      </thead>
      <% if assets.any? %>
        <% @grid.assets.each do |asset| %>
          <% @grid.tiers.each do |tier| %>
            <tbody class="js-tier" data-tier="<%= tier %>" >
              <%= datagrid_row(grid, asset, columns: grid.columns_by_tier(tier)) %>
            </tbody>
          <% end %>
        <% end %>
      <% else %>
        <tr><td class="noresults" colspan="100%"><%= I18n.t('datagrid.no_results').html_safe %></td></tr>
      <% end %>
    <% end %>
  <% else -%>
    <%= I18n.t("datagrid.table.no_columns").html_safe %>
  <% end %>
</div>
<tr class="bg-white border-b ">
  <% grid.columns.each do |column| %>
    <td scope="row" class="<%= datagrid_column_classes(grid, column) + " px-6 py-4 mx-20" %>">
      <%= datagrid_value(grid, column, asset) rescue binding.pry %>
    </td>
  <% end %>
</tr>
bogdan commented 7 months ago

In _row.html.erb, change:

  <% grid.columns.each do |column| %>
  <%# to %>
  <% grid.html_columns(*options[:columns]).each do |column| %>

Not sure why it is grid.columns for you: https://github.com/bogdan/datagrid/blob/master/app/views/datagrid/_row.html.erb#L2

fzf commented 7 months ago

@bogdan sorry for the confusion there I had updated that line to get past the following error and at least getting it rendering. Screenshot 2024-01-14 at 08 44 47 It looks like it is failing here

bogdan commented 7 months ago

It is likely a bug. Try the following workaround:

              <%= datagrid_row(grid, asset, columns: grid.columns_by_tier(tier)).map(&:name) %>

I'll fix it in the next version anyway

fzf commented 7 months ago

Thanks for getting back to me so quick with fixes. I am still not getting the behavior I think should happen so I made this quick project just run bundle then rails db:setup and rails s to get it up and running. With this config:

  tiered_columns(:col1, {header: "Col 1"}, [
    -> (model) { model.first_name },
    -> (model) { model.last_name },
  ])
  tiered_columns(:col2, {header: "Col 2"}, [
    -> (model) { model.age },
    -> (model) { model.job },
  ])

With the HTML changes Screenshot 2024-01-14 at 14 10 44

I would expect:

Col1   Col2
Test   42
User   1337 Hacker

Let me know if there is any more information you need here.

bogdan commented 7 months ago

Made it work like so:

diff --git a/app/grids/users_grid.rb b/app/grids/users_grid.rb
index 18e8578..feaa837 100644
--- a/app/grids/users_grid.rb
+++ b/app/grids/users_grid.rb
@@ -12,7 +12,7 @@ class UsersGrid < BaseGrid
   def self.tiered_columns(name, options, blocks)
     tiers.each do |tier|
       block = blocks[tier] || -> (_) { '--blank--' }
-      column(name, **options, tier: tier) do |model|
+      column(:"#{name}_#{tier}", **options, tier: tier) do |model|
         block.call(model)
       end
     end
@@ -27,8 +27,8 @@ class UsersGrid < BaseGrid
     -> (model) { model.job },
   ])

-  def columns_by_tier(tier)
-    columns.select{|c| c.options[:tier] == tier}
+  def column_names_by_tier(tier)
+    columns.select{|c| c.options[:tier] == tier}.map(&:name)
   end

   def tiers
diff --git a/app/views/datagrid/_table.html.erb b/app/views/datagrid/_table.html.erb
index e47bc96..1997264 100644
--- a/app/views/datagrid/_table.html.erb
+++ b/app/views/datagrid/_table.html.erb
@@ -7,13 +7,13 @@ Local variables:
 <% if grid.html_columns(*options[:columns]).any? %>
   <%= content_tag :table, options[:html] do %>
     <thead>
-      <%= datagrid_header(grid, options) %>
+      <%= datagrid_header(grid, **options, columns: grid.column_names_by_tier(0)) %>
     </thead>
       <% if assets.any? %>
        <% @grid.assets.each do |asset| %>
           <% @grid.tiers.each do |tier| %>
             <tbody class="js-tier" data-tier="<%= tier %>" >
-              <%= datagrid_row(grid, asset, columns: grid.columns_by_tier(tier).map(&:name)) %>
+              <%= datagrid_row(grid, asset, **options, columns: grid.column_names_by_tier(tier)) %>
             </tbody>
           <% end %>
         <% end %>    
fzf commented 7 months ago

Thank you! This got it working, the other columns no longer show up but I can work around that.