activeadmin_table_footer 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/LICENSE.txt +21 -0
- data/README.md +64 -0
- data/lib/activeadmin_table_footer/engine.rb +15 -0
- data/lib/activeadmin_table_footer/index_as_table_extension.rb +23 -0
- data/lib/activeadmin_table_footer/styles.rb +21 -0
- data/lib/activeadmin_table_footer/table_for_extension.rb +125 -0
- data/lib/activeadmin_table_footer/version.rb +5 -0
- data/lib/activeadmin_table_footer.rb +24 -0
- metadata +106 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 3fa5ab83f8cc3ef9268b501ee4fd09d7fb3d1b6cab32b83dccc80a888bdec44e
|
|
4
|
+
data.tar.gz: 506c5310ec7706b0314641334ab4558b92a2cdcd37e9d861fb6b6ee17adf1c73
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: c415a857de380ae03c0f90f3d0d47ecfb2b55fa7e8569f009bc6f4b550127ac11b3a39911e8f785738d03227abaeed6d614be605acd28b04d4cc2d0bc926c946
|
|
7
|
+
data.tar.gz: 9498192db2a62f42d2f52f9e68a6e2aa038d4c2c9e5448e450128c4c4cb43cfecaa5ed4dab29c9dabf039c375ab9bb46ef58c295adb7bd5c7a9104dafb075653
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Igor Fedoronchuk
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
# activeadmin_table_footer
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
Adds a `<tfoot>` row to ActiveAdmin index tables. Totals are aggregated
|
|
8
|
+
across **all pages** of the filtered scope — not just the visible one — in a
|
|
9
|
+
single SQL query when you use `footer_data:`.
|
|
10
|
+
|
|
11
|
+
Works with **ActiveAdmin 3.5+ and 4.x**.
|
|
12
|
+
|
|
13
|
+
### ActiveAdmin 4
|
|
14
|
+
|
|
15
|
+

|
|
16
|
+
|
|
17
|
+
### ActiveAdmin 3
|
|
18
|
+
|
|
19
|
+

|
|
20
|
+
|
|
21
|
+
The page shows 30 rows, but the footer row reports the sum across all 42
|
|
22
|
+
subscriptions — that's the point.
|
|
23
|
+
|
|
24
|
+
## Install
|
|
25
|
+
|
|
26
|
+
```ruby
|
|
27
|
+
# Gemfile
|
|
28
|
+
gem "activeadmin_table_footer"
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Usage
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
ActiveAdmin.register Subscription do
|
|
35
|
+
index footer_data: ->(collection) {
|
|
36
|
+
totals = collection.joins(:plan).pick(
|
|
37
|
+
Arel.sql("COALESCE(SUM(seats), 0)"),
|
|
38
|
+
Arel.sql("COALESCE(SUM(seats * plans.monthly_price), 0)")
|
|
39
|
+
)
|
|
40
|
+
{ total_seats: totals[0], total_cost: totals[1] }
|
|
41
|
+
} do
|
|
42
|
+
column :customer
|
|
43
|
+
column :plan
|
|
44
|
+
column :is_operator, footer: -> { strong { "Total (all pages)" } }
|
|
45
|
+
column "Seats", footer: -> { strong { footer_data[:total_seats].to_s } }, &:seats
|
|
46
|
+
column :total_cost, footer: -> { strong { number_to_currency(footer_data[:total_cost]) } } do |row|
|
|
47
|
+
number_to_currency row.total_cost
|
|
48
|
+
end
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
The `footer_data:` Proc runs once over the filtered scope (LIMIT/OFFSET/ORDER
|
|
54
|
+
are stripped automatically). The result is exposed inside each
|
|
55
|
+
`column …, footer: …` Proc via `footer_data`.
|
|
56
|
+
|
|
57
|
+
`footer:` accepts a string, a symbol (`:sum`, `:count`, `:average`,
|
|
58
|
+
`:minimum`, `:maximum`), or a Proc — Procs run inside the table view, so view
|
|
59
|
+
helpers (`number_to_currency`, `link_to`) and Arbre tags (`strong`, `span`)
|
|
60
|
+
work as expected.
|
|
61
|
+
|
|
62
|
+
## License
|
|
63
|
+
|
|
64
|
+
MIT
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "rails/engine"
|
|
4
|
+
|
|
5
|
+
module ActiveadminTableFooter
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
config.to_prepare do
|
|
8
|
+
require "activeadmin_table_footer/table_for_extension"
|
|
9
|
+
require "activeadmin_table_footer/index_as_table_extension"
|
|
10
|
+
|
|
11
|
+
ActiveAdmin::Views::TableFor.prepend(ActiveadminTableFooter::TableForExtension)
|
|
12
|
+
ActiveAdmin::Views::IndexAsTable.prepend(ActiveadminTableFooter::IndexAsTableExtension)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminTableFooter
|
|
4
|
+
# IndexAsTable#build constructs its own table_options hash and does not pass
|
|
5
|
+
# unknown options through. We wrap the user block so the TableFor instance
|
|
6
|
+
# receives @footer_data_proc before columns are evaluated.
|
|
7
|
+
module IndexAsTableExtension
|
|
8
|
+
def build(page_presenter, collection)
|
|
9
|
+
footer_proc = page_presenter[:footer_data]
|
|
10
|
+
|
|
11
|
+
if footer_proc && page_presenter.block
|
|
12
|
+
original_block = page_presenter.block
|
|
13
|
+
wrapped = lambda do |table|
|
|
14
|
+
table.instance_variable_set(:@footer_data_proc, footer_proc)
|
|
15
|
+
instance_exec(table, &original_block)
|
|
16
|
+
end
|
|
17
|
+
page_presenter.instance_variable_set(:@block, wrapped)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
super
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminTableFooter
|
|
4
|
+
module Styles
|
|
5
|
+
TAILWIND_TH = "px-3 py-2 bg-gray-50 dark:bg-gray-800/50 font-semibold border-t border-gray-200 dark:border-gray-700 text-left"
|
|
6
|
+
|
|
7
|
+
module_function
|
|
8
|
+
|
|
9
|
+
def aa_v4?
|
|
10
|
+
Gem::Version.new(ActiveAdmin::VERSION) >= Gem::Version.new("4.0.0.beta1")
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def footer_th_class
|
|
14
|
+
aa_v4? ? TAILWIND_TH : ""
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def footer_tr_class
|
|
18
|
+
aa_v4? ? "" : "footer"
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveadminTableFooter
|
|
4
|
+
module TableForExtension
|
|
5
|
+
def build(obj, *attrs)
|
|
6
|
+
options = attrs.extract_options!
|
|
7
|
+
@footer_data_proc = options.delete(:footer_data)
|
|
8
|
+
super(obj, *attrs, options)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def footer_data
|
|
12
|
+
return @footer_data if defined?(@footer_data)
|
|
13
|
+
return (@footer_data = nil) unless @footer_data_proc
|
|
14
|
+
@footer_data = @footer_data_proc.call(unscoped_collection_for_footer)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# The collection AA passes us is the paginated + ordered slice. Aggregates
|
|
18
|
+
# over "all rows" need the underlying relation without LIMIT/OFFSET/ORDER —
|
|
19
|
+
# otherwise SUM/COUNT on page 2 would only see the page-2 slice and ORDER
|
|
20
|
+
# confuses some aggregate queries.
|
|
21
|
+
def unscoped_collection_for_footer
|
|
22
|
+
return @collection unless @collection.respond_to?(:except)
|
|
23
|
+
@collection.except(:limit, :offset, :order)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def column(*args, &block)
|
|
27
|
+
super
|
|
28
|
+
col = @columns.last
|
|
29
|
+
|
|
30
|
+
if @aatf_footer_row
|
|
31
|
+
# Tfoot already exists — every subsequent column gets a cell
|
|
32
|
+
# (with content if it has :footer, empty otherwise) so columns align.
|
|
33
|
+
build_footer_cell_for(col)
|
|
34
|
+
elsif column_has_footer?(col)
|
|
35
|
+
# First column with :footer — open tfoot and back-fill empty cells
|
|
36
|
+
# for all previously-added columns so the row aligns with headers.
|
|
37
|
+
ensure_tfoot!
|
|
38
|
+
@columns[0...-1].each { |prior| build_footer_cell_for(prior) }
|
|
39
|
+
build_footer_cell_for(col)
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
private
|
|
44
|
+
|
|
45
|
+
def column_has_footer?(col)
|
|
46
|
+
col.instance_variable_get(:@options).key?(:footer)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def build_footer_cell_for(col)
|
|
50
|
+
within @aatf_footer_row do
|
|
51
|
+
column_key = column_key_for(col)
|
|
52
|
+
# Always include `col col-<key>` so capybara_active_admin matchers
|
|
53
|
+
# (`have_table_cell(column: ...)`) work in both AA 3 and AA 4 — the
|
|
54
|
+
# selector convention is `td.col.col-<key>`.
|
|
55
|
+
compat_classes = column_key ? "col col-#{column_key}" : "col"
|
|
56
|
+
classes = [col.html_class, compat_classes, ActiveadminTableFooter.footer_th_class]
|
|
57
|
+
.reject { |c| c.nil? || c.to_s.empty? }
|
|
58
|
+
.join(" ")
|
|
59
|
+
attrs = { class: classes.empty? ? nil : classes }
|
|
60
|
+
attrs[:"data-column"] = column_key if column_key
|
|
61
|
+
td(**attrs) do
|
|
62
|
+
render_footer_value(col) if column_has_footer?(col)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def column_key_for(col)
|
|
68
|
+
# AA 4 exposes Column#title_id; AA 3 does not — derive from title.
|
|
69
|
+
if col.respond_to?(:title_id) && col.title_id.respond_to?(:presence)
|
|
70
|
+
return col.title_id.presence
|
|
71
|
+
end
|
|
72
|
+
title = col.title.to_s
|
|
73
|
+
return nil if title.empty?
|
|
74
|
+
title.parameterize(separator: "_")
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def ensure_tfoot!
|
|
78
|
+
return if @aatf_footer_row
|
|
79
|
+
tfoot_classes = ActiveadminTableFooter.footer_tr_class
|
|
80
|
+
tfoot do
|
|
81
|
+
@aatf_footer_row = tr(class: tfoot_classes.presence)
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
def render_footer_value(col)
|
|
86
|
+
footer = col.instance_variable_get(:@options)[:footer]
|
|
87
|
+
case footer
|
|
88
|
+
when nil
|
|
89
|
+
nil
|
|
90
|
+
when Symbol
|
|
91
|
+
text_node aggregate_collection(footer, col.data).to_s
|
|
92
|
+
when Proc
|
|
93
|
+
arg = unscoped_collection_for_footer
|
|
94
|
+
result = footer.arity == 0 ? instance_exec(&footer) : instance_exec(arg, &footer)
|
|
95
|
+
text_node(result.to_s) unless result.is_a?(Arbre::Element)
|
|
96
|
+
else
|
|
97
|
+
text_node footer.to_s
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
# Symbol footer (`:sum`, `:count`, ...) works for both AR relations and
|
|
102
|
+
# plain Arrays. AR uses native SQL aggregates; Arrays fall back to
|
|
103
|
+
# in-Ruby Enumerable equivalents.
|
|
104
|
+
def aggregate_collection(method, attribute)
|
|
105
|
+
scope = unscoped_collection_for_footer
|
|
106
|
+
if ar_relation?(scope)
|
|
107
|
+
scope.public_send(method, attribute)
|
|
108
|
+
else
|
|
109
|
+
values = Array(scope).map { |r| r.public_send(attribute) }.compact
|
|
110
|
+
case method
|
|
111
|
+
when :sum then values.sum
|
|
112
|
+
when :count then values.size
|
|
113
|
+
when :average then values.empty? ? 0 : values.sum.to_f / values.size
|
|
114
|
+
when :minimum then values.min
|
|
115
|
+
when :maximum then values.max
|
|
116
|
+
else scope.public_send(method, attribute)
|
|
117
|
+
end
|
|
118
|
+
end
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def ar_relation?(scope)
|
|
122
|
+
defined?(ActiveRecord::Relation) && scope.is_a?(ActiveRecord::Relation)
|
|
123
|
+
end
|
|
124
|
+
end
|
|
125
|
+
end
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "activeadmin_table_footer/version"
|
|
4
|
+
require "activeadmin_table_footer/styles"
|
|
5
|
+
|
|
6
|
+
module ActiveadminTableFooter
|
|
7
|
+
class << self
|
|
8
|
+
attr_writer :footer_th_class, :footer_tr_class
|
|
9
|
+
|
|
10
|
+
def footer_th_class
|
|
11
|
+
@footer_th_class || Styles.footer_th_class
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def footer_tr_class
|
|
15
|
+
@footer_tr_class || Styles.footer_tr_class
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def configure
|
|
19
|
+
yield self
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
require "activeadmin_table_footer/engine" if defined?(Rails)
|
metadata
ADDED
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activeadmin_table_footer
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Igor Fedoronchuk
|
|
8
|
+
bindir: bin
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: activeadmin
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '3.5'
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '5.0'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: '3.5'
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '5.0'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: arbre
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '1.4'
|
|
39
|
+
- - "<"
|
|
40
|
+
- !ruby/object:Gem::Version
|
|
41
|
+
version: '3.0'
|
|
42
|
+
type: :runtime
|
|
43
|
+
prerelease: false
|
|
44
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
45
|
+
requirements:
|
|
46
|
+
- - ">="
|
|
47
|
+
- !ruby/object:Gem::Version
|
|
48
|
+
version: '1.4'
|
|
49
|
+
- - "<"
|
|
50
|
+
- !ruby/object:Gem::Version
|
|
51
|
+
version: '3.0'
|
|
52
|
+
- !ruby/object:Gem::Dependency
|
|
53
|
+
name: railties
|
|
54
|
+
requirement: !ruby/object:Gem::Requirement
|
|
55
|
+
requirements:
|
|
56
|
+
- - ">="
|
|
57
|
+
- !ruby/object:Gem::Version
|
|
58
|
+
version: '7.0'
|
|
59
|
+
type: :runtime
|
|
60
|
+
prerelease: false
|
|
61
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
62
|
+
requirements:
|
|
63
|
+
- - ">="
|
|
64
|
+
- !ruby/object:Gem::Version
|
|
65
|
+
version: '7.0'
|
|
66
|
+
description: Adds a `footer:` option to columns and a top-level `footer_data:` proc
|
|
67
|
+
so index tables can render a <tfoot> with aggregated values in a single SQL query.
|
|
68
|
+
email:
|
|
69
|
+
- fedoronchuk@gmail.com
|
|
70
|
+
executables: []
|
|
71
|
+
extensions: []
|
|
72
|
+
extra_rdoc_files: []
|
|
73
|
+
files:
|
|
74
|
+
- LICENSE.txt
|
|
75
|
+
- README.md
|
|
76
|
+
- lib/activeadmin_table_footer.rb
|
|
77
|
+
- lib/activeadmin_table_footer/engine.rb
|
|
78
|
+
- lib/activeadmin_table_footer/index_as_table_extension.rb
|
|
79
|
+
- lib/activeadmin_table_footer/styles.rb
|
|
80
|
+
- lib/activeadmin_table_footer/table_for_extension.rb
|
|
81
|
+
- lib/activeadmin_table_footer/version.rb
|
|
82
|
+
homepage: https://github.com/activeadmin-plugins/activeadmin_table_footer
|
|
83
|
+
licenses:
|
|
84
|
+
- MIT
|
|
85
|
+
metadata:
|
|
86
|
+
homepage_uri: https://github.com/activeadmin-plugins/activeadmin_table_footer
|
|
87
|
+
source_code_uri: https://github.com/activeadmin-plugins/activeadmin_table_footer
|
|
88
|
+
changelog_uri: https://github.com/activeadmin-plugins/activeadmin_table_footer/releases
|
|
89
|
+
rdoc_options: []
|
|
90
|
+
require_paths:
|
|
91
|
+
- lib
|
|
92
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '3.1'
|
|
97
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
98
|
+
requirements:
|
|
99
|
+
- - ">="
|
|
100
|
+
- !ruby/object:Gem::Version
|
|
101
|
+
version: '0'
|
|
102
|
+
requirements: []
|
|
103
|
+
rubygems_version: 3.7.1
|
|
104
|
+
specification_version: 4
|
|
105
|
+
summary: Table footer DSL for ActiveAdmin index tables
|
|
106
|
+
test_files: []
|