activeadmin_configurable_columns 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/CHANGELOG.md +13 -0
- data/LICENSE.txt +21 -0
- data/README.md +170 -0
- data/app/models/activeadmin_configurable_columns/preference.rb +47 -0
- data/app/views/activeadmin_configurable_columns/_form.html.erb +25 -0
- data/app/views/activeadmin_configurable_columns/_page.html.erb +16 -0
- data/app/views/activeadmin_configurable_columns/_sidebar.html.erb +2 -0
- data/config/locales/en.yml +10 -0
- data/db/migrate/20260930000001_create_activeadmin_configurable_columns_preferences.rb +18 -0
- data/lib/activeadmin_configurable_columns/admin/table_columns.rb +37 -0
- data/lib/activeadmin_configurable_columns/column.rb +49 -0
- data/lib/activeadmin_configurable_columns/dsl.rb +36 -0
- data/lib/activeadmin_configurable_columns/engine.rb +21 -0
- data/lib/activeadmin_configurable_columns/registry.rb +46 -0
- data/lib/activeadmin_configurable_columns/resource_extension.rb +10 -0
- data/lib/activeadmin_configurable_columns/table_renderer.rb +18 -0
- data/lib/activeadmin_configurable_columns/version.rb +3 -0
- data/lib/activeadmin_configurable_columns.rb +98 -0
- metadata +113 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: bdf4162f0335b3294ed14cc99cc72ed22ce80e798276f9903e3f6a55fe2858f6
|
|
4
|
+
data.tar.gz: 6082979b4e9332a4f3d904b6ae1838855515d4a26756ba6dc2f54edd0cb06720
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: 33db6eb5c5af30b9fc6e092942549b791fbf48f9e691daa3ee9d273f30b0af3bb41f55137429a626465497cf83a35f5bfadb8683e47fc86c837e80a14c66b33b
|
|
7
|
+
data.tar.gz: 02a8485aa3eaac9ea24037f261ec15e82b519f216b5de39e61057fdbb7582face1cc01cb18ad7fd89854d176e2440f2fb85f7f8abc07423f3de32f2f5d5e1959
|
data/CHANGELOG.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
## 1.0.0 (2026-09-30)
|
|
4
|
+
|
|
5
|
+
Initial release.
|
|
6
|
+
|
|
7
|
+
- `configurable_columns` DSL to declare the columns an admin may pick from.
|
|
8
|
+
- `configurable_columns` index-table renderer that shows the columns the
|
|
9
|
+
current admin chose, falling back to the declared defaults.
|
|
10
|
+
- "Columns" picker in the sidebar of every configurable index.
|
|
11
|
+
- "Table columns" admin page to review and change every table at once.
|
|
12
|
+
- Per-admin persistence in the `activeadmin_configurable_columns_preferences`
|
|
13
|
+
table, with a pluggable preference store.
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Kyryl Oliinyk
|
|
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
|
|
13
|
+
all 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
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# activeadmin_configurable_columns
|
|
2
|
+
|
|
3
|
+
[](https://github.com/kyryloliinyk/activeadmin_configurable_columns/actions/workflows/ci.yml)
|
|
4
|
+
|
|
5
|
+
Per-admin configurable index table columns for [ActiveAdmin](https://activeadmin.info) 4.
|
|
6
|
+
|
|
7
|
+
ActiveAdmin has no built-in way to let each admin choose which columns of an
|
|
8
|
+
index table they see. This gem adds one, built on ActiveAdmin's own extension
|
|
9
|
+
points:
|
|
10
|
+
|
|
11
|
+
- a `configurable_columns do ... end` DSL declares the columns an admin may pick from;
|
|
12
|
+
- `configurable_columns` inside an `index` block renders the ones the current admin chose;
|
|
13
|
+
- a "Columns" picker appears in the sidebar of every configurable index;
|
|
14
|
+
- a "Table columns" admin page lets an admin review and change every table at once;
|
|
15
|
+
- each admin's choice is stored in the database, per admin and per table.
|
|
16
|
+
|
|
17
|
+
## Requirements
|
|
18
|
+
|
|
19
|
+
- Ruby >= 3.1
|
|
20
|
+
- Rails >= 7.1
|
|
21
|
+
- ActiveAdmin 4.0 (beta15 or later)
|
|
22
|
+
|
|
23
|
+
## Installation
|
|
24
|
+
|
|
25
|
+
Add the gem to your Gemfile:
|
|
26
|
+
|
|
27
|
+
```ruby
|
|
28
|
+
gem 'activeadmin_configurable_columns'
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
Install the migration and migrate:
|
|
32
|
+
|
|
33
|
+
```sh
|
|
34
|
+
bin/rails activeadmin_configurable_columns:install:migrations
|
|
35
|
+
bin/rails db:migrate
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
This creates the `activeadmin_configurable_columns_preferences` table. The
|
|
39
|
+
`admin` reference is polymorphic with a string id, so it works with any
|
|
40
|
+
current-user model and any primary key type (integer, uuid, ...).
|
|
41
|
+
|
|
42
|
+
## Usage
|
|
43
|
+
|
|
44
|
+
Declare the available columns in an ActiveAdmin registration, and render them
|
|
45
|
+
in the index:
|
|
46
|
+
|
|
47
|
+
```ruby
|
|
48
|
+
ActiveAdmin.register Product do
|
|
49
|
+
configurable_columns do
|
|
50
|
+
column :brand
|
|
51
|
+
column :sku, label: 'SKU'
|
|
52
|
+
column(:qty, sortable: 'stock_items_quantity') { |product| product.total_quantity }
|
|
53
|
+
column :notes, default: false # available, but hidden until picked
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
index do
|
|
57
|
+
selectable_column
|
|
58
|
+
configurable_columns
|
|
59
|
+
actions
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
```
|
|
63
|
+
|
|
64
|
+
`column` mirrors the arguments of ActiveAdmin's own `column`, so a declaration
|
|
65
|
+
reads the same as the index DSL it replaces:
|
|
66
|
+
|
|
67
|
+
| Argument | Meaning |
|
|
68
|
+
| --- | --- |
|
|
69
|
+
| `key` | Identifies the column in the picker and in stored preferences. Also the rendered attribute unless `attribute:` or a block says otherwise. |
|
|
70
|
+
| `label:` | Column header and checkbox label. Defaults to the model's `human_attribute_name`. |
|
|
71
|
+
| `default:` | Whether the column is visible for admins who never picked anything. Defaults to `true`. |
|
|
72
|
+
| `attribute:` | The attribute to render when it differs from the key. |
|
|
73
|
+
| a block | Renders the cell. The block is evaluated against the index table, so view helpers work, exactly as in a regular `index` column. |
|
|
74
|
+
| anything else | Passed through to ActiveAdmin's `column` (e.g. `sortable:`, `class:`). |
|
|
75
|
+
|
|
76
|
+
Every resource that declares `configurable_columns` gets a "Columns" sidebar
|
|
77
|
+
section on its index, and a card on the "Table columns" page. Selections are
|
|
78
|
+
saved per admin; unknown or stale keys are dropped on read and write, and an
|
|
79
|
+
empty selection falls back to the declared defaults, so an admin can never end
|
|
80
|
+
up with an empty table.
|
|
81
|
+
|
|
82
|
+
## Configuration
|
|
83
|
+
|
|
84
|
+
All settings are optional. In `config/initializers/activeadmin_configurable_columns.rb`:
|
|
85
|
+
|
|
86
|
+
```ruby
|
|
87
|
+
ActiveadminConfigurableColumns.configure do |config|
|
|
88
|
+
# Options for the "Table columns" page's `menu` call, or false to keep the
|
|
89
|
+
# page out of the navigation. Default: a translated label, top level.
|
|
90
|
+
config.menu = { parent: 'Settings', priority: 0 }
|
|
91
|
+
|
|
92
|
+
# Priority of the "Columns" sidebar section. ActiveAdmin's filters sidebar
|
|
93
|
+
# uses 10; the default (20) renders right below it.
|
|
94
|
+
config.sidebar_priority = 20
|
|
95
|
+
|
|
96
|
+
# Where selections are read from and written to. Any object answering
|
|
97
|
+
# visible_column_keys(admin, resource_key) -> [Symbol] | nil (nil = never saved)
|
|
98
|
+
# store(admin:, resource_key:, keys:)
|
|
99
|
+
# can be plugged in. Default: the bundled Preference model.
|
|
100
|
+
config.preference_store = MyOwnStore
|
|
101
|
+
end
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
The current admin is whatever ActiveAdmin's `current_active_admin_user`
|
|
105
|
+
returns, so the gem follows your `config.current_user_method` setting.
|
|
106
|
+
|
|
107
|
+
## Translations
|
|
108
|
+
|
|
109
|
+
The gem ships English strings under standard ActiveAdmin keys; override them
|
|
110
|
+
per locale in your application:
|
|
111
|
+
|
|
112
|
+
```yaml
|
|
113
|
+
en:
|
|
114
|
+
active_admin:
|
|
115
|
+
sidebars:
|
|
116
|
+
columns: "Columns"
|
|
117
|
+
table_columns:
|
|
118
|
+
title: "Table columns"
|
|
119
|
+
save: "Save columns"
|
|
120
|
+
saved: "Columns saved"
|
|
121
|
+
empty: "No table has configurable columns yet."
|
|
122
|
+
unknown_resource: "Unknown table"
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
Column checkbox labels come from `human_attribute_name`, so your existing
|
|
126
|
+
Active Record attribute translations apply.
|
|
127
|
+
|
|
128
|
+
## Tailwind CSS
|
|
129
|
+
|
|
130
|
+
ActiveAdmin 4 builds its stylesheet with Tailwind, which only emits classes it
|
|
131
|
+
finds in scanned sources. Add the gem's views to your Tailwind entrypoint (or
|
|
132
|
+
config) so the picker and the "Table columns" page keep their layout:
|
|
133
|
+
|
|
134
|
+
```css
|
|
135
|
+
@source "<path to the installed gem>/app/views/**/*.{arb,erb,html,rb}";
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
If a rake task generates your ActiveAdmin Tailwind entrypoint, resolve the path
|
|
139
|
+
with `Gem.loaded_specs['activeadmin_configurable_columns'].full_gem_path`.
|
|
140
|
+
|
|
141
|
+
## How it works
|
|
142
|
+
|
|
143
|
+
- `ActiveadminConfigurableColumns::Dsl` is mixed into `ActiveAdmin::ResourceDSL`;
|
|
144
|
+
`configurable_columns do ... end` builds a `Registry` of `Column`s, stores it on
|
|
145
|
+
the resource, and adds the sidebar section.
|
|
146
|
+
- `ActiveadminConfigurableColumns::TableRenderer` is mixed into
|
|
147
|
+
`ActiveAdmin::Views::IndexAsTable::IndexTableFor`; `configurable_columns` inside
|
|
148
|
+
`index` looks up the current admin's saved keys and emits the matching columns.
|
|
149
|
+
- The "Table columns" page is a regular `ActiveAdmin.register_page`, loaded
|
|
150
|
+
through ActiveAdmin's own load paths; its update action also serves the
|
|
151
|
+
sidebar forms.
|
|
152
|
+
- Rendering an index whose resource never declared its columns raises
|
|
153
|
+
`ActiveadminConfigurableColumns::NotDeclared` with a hint, rather than an
|
|
154
|
+
empty table.
|
|
155
|
+
|
|
156
|
+
## Development
|
|
157
|
+
|
|
158
|
+
```sh
|
|
159
|
+
bundle install
|
|
160
|
+
bundle exec rspec
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
The test suite is standalone: it boots a dummy Rails application
|
|
164
|
+
(`spec/dummy`) with an in-memory sqlite database and a real ActiveAdmin
|
|
165
|
+
registration, and exercises the gem's migration on every run.
|
|
166
|
+
|
|
167
|
+
## License
|
|
168
|
+
|
|
169
|
+
The gem is available as open source under the terms of the
|
|
170
|
+
[MIT License](LICENSE.txt).
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# One row per admin per ActiveAdmin index table, holding the columns that
|
|
3
|
+
# admin chose to see there. The available columns themselves are declared in
|
|
4
|
+
# the ActiveAdmin registration with the `configurable_columns` DSL.
|
|
5
|
+
#
|
|
6
|
+
# `admin` is polymorphic and its id column is a string, so any current-user
|
|
7
|
+
# model works, whatever its class and primary key type (integer, uuid, ...).
|
|
8
|
+
class Preference < ::ActiveRecord::Base
|
|
9
|
+
self.table_name = 'activeadmin_configurable_columns_preferences'
|
|
10
|
+
|
|
11
|
+
belongs_to :admin, polymorphic: true
|
|
12
|
+
|
|
13
|
+
# Stored as JSON text rather than a database-specific json type, so the
|
|
14
|
+
# table works the same on every Active Record adapter. The array shape is
|
|
15
|
+
# enforced by the validation below, not by the coder, so a bad assignment
|
|
16
|
+
# surfaces as a validation error instead of a serialization exception.
|
|
17
|
+
serialize :visible_columns, coder: JSON
|
|
18
|
+
|
|
19
|
+
after_initialize { self.visible_columns ||= [] if has_attribute?(:visible_columns) }
|
|
20
|
+
|
|
21
|
+
validates :resource_key, presence: true,
|
|
22
|
+
uniqueness: { scope: %i[admin_type admin_id], case_sensitive: false }
|
|
23
|
+
validate :visible_columns_must_be_an_array
|
|
24
|
+
|
|
25
|
+
# Returns the keys this admin picked for the table, or nil when they never
|
|
26
|
+
# saved anything (the caller then falls back to the declared defaults).
|
|
27
|
+
def self.visible_column_keys(admin, resource_key)
|
|
28
|
+
return nil if admin.blank?
|
|
29
|
+
|
|
30
|
+
find_by(admin: admin, resource_key: resource_key)&.visible_columns&.map(&:to_sym)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def self.store(admin:, resource_key:, keys:)
|
|
34
|
+
preference = find_or_initialize_by(admin: admin, resource_key: resource_key)
|
|
35
|
+
preference.update!(visible_columns: keys.map(&:to_s))
|
|
36
|
+
preference
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def visible_columns_must_be_an_array
|
|
42
|
+
return if visible_columns.is_a?(Array)
|
|
43
|
+
|
|
44
|
+
errors.add(:visible_columns, :invalid)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
<%# Checkbox picker for one index table. Rendered both in the index sidebar and
|
|
2
|
+
on the "Table columns" page, so it always posts back to the same action. %>
|
|
3
|
+
<% registry = resource.column_registry %>
|
|
4
|
+
<% selected = ActiveadminConfigurableColumns.visible_columns(registry, current_active_admin_user).map(&:key) %>
|
|
5
|
+
<% update_path = send(ActiveadminConfigurableColumns.route_name(active_admin_namespace, :update)) %>
|
|
6
|
+
|
|
7
|
+
<%= form_with url: update_path, method: :patch, class: 'filters-form' do %>
|
|
8
|
+
<%= hidden_field_tag :resource_key, registry.resource_key, id: nil %>
|
|
9
|
+
<%# Keeps the parameter present when every box is unchecked, which restores the defaults. %>
|
|
10
|
+
<%= hidden_field_tag 'visible_columns[]', '', id: nil %>
|
|
11
|
+
|
|
12
|
+
<div class="flex flex-col gap-2 mb-4">
|
|
13
|
+
<% registry.columns.each do |column| %>
|
|
14
|
+
<% checkbox_id = "#{registry.resource_key}_column_#{column.key}" %>
|
|
15
|
+
<div class="flex items-center gap-2">
|
|
16
|
+
<%= check_box_tag 'visible_columns[]', column.key, selected.include?(column.key), id: checkbox_id %>
|
|
17
|
+
<%= label_tag checkbox_id, column.human_label(resource.resource_class), class: 'text-sm' %>
|
|
18
|
+
</div>
|
|
19
|
+
<% end %>
|
|
20
|
+
</div>
|
|
21
|
+
|
|
22
|
+
<div class="filters-form-buttons">
|
|
23
|
+
<%= submit_tag t('active_admin.table_columns.save'), class: 'filters-form-submit' %>
|
|
24
|
+
</div>
|
|
25
|
+
<% end %>
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
<% if resources.empty? %>
|
|
2
|
+
<div class="panel">
|
|
3
|
+
<div class="panel-body"><%= t('active_admin.table_columns.empty') %></div>
|
|
4
|
+
</div>
|
|
5
|
+
<% else %>
|
|
6
|
+
<div class="grid gap-6 md:grid-cols-2 xl:grid-cols-3">
|
|
7
|
+
<% resources.each do |resource| %>
|
|
8
|
+
<div class="panel">
|
|
9
|
+
<h3 class="panel-title"><%= resource.plural_resource_label %></h3>
|
|
10
|
+
<div class="panel-body">
|
|
11
|
+
<%= render 'activeadmin_configurable_columns/form', resource: resource %>
|
|
12
|
+
</div>
|
|
13
|
+
</div>
|
|
14
|
+
<% end %>
|
|
15
|
+
</div>
|
|
16
|
+
<% end %>
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
class CreateActiveadminConfigurableColumnsPreferences < ActiveRecord::Migration[7.1]
|
|
2
|
+
def change
|
|
3
|
+
create_table :activeadmin_configurable_columns_preferences do |t|
|
|
4
|
+
# A string id fits every primary key type a current-user model may have.
|
|
5
|
+
t.string :admin_type, null: false
|
|
6
|
+
t.string :admin_id, null: false
|
|
7
|
+
t.string :resource_key, null: false
|
|
8
|
+
# JSON-serialized array of column keys; plain text so every adapter works.
|
|
9
|
+
t.text :visible_columns
|
|
10
|
+
t.timestamps
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
add_index :activeadmin_configurable_columns_preferences,
|
|
14
|
+
%i[admin_type admin_id resource_key],
|
|
15
|
+
unique: true,
|
|
16
|
+
name: 'index_aacc_preferences_on_admin_and_resource_key'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# The "Table columns" page: a central place to review and change visible table
|
|
2
|
+
# columns at once. The same form is rendered in the "Columns" sidebar of every
|
|
3
|
+
# configurable index, and both post back to the page's update action.
|
|
4
|
+
ActiveAdmin.register_page 'Table columns' do
|
|
5
|
+
menu ActiveadminConfigurableColumns.menu
|
|
6
|
+
|
|
7
|
+
content title: proc { I18n.t('active_admin.table_columns.title') } do
|
|
8
|
+
resources = ActiveadminConfigurableColumns.resources(active_admin_namespace).select do |resource|
|
|
9
|
+
authorized?(ActiveAdmin::Authorization::READ, resource.resource_class)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
render 'activeadmin_configurable_columns/page', resources: resources
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
page_action :update, method: :patch do
|
|
16
|
+
page_path = send(ActiveadminConfigurableColumns.route_name(active_admin_namespace))
|
|
17
|
+
resource = ActiveadminConfigurableColumns.resource_for(active_admin_namespace, params[:resource_key])
|
|
18
|
+
|
|
19
|
+
if resource.nil?
|
|
20
|
+
redirect_back fallback_location: page_path,
|
|
21
|
+
status: :see_other,
|
|
22
|
+
alert: t('active_admin.table_columns.unknown_resource')
|
|
23
|
+
else
|
|
24
|
+
registry = resource.column_registry
|
|
25
|
+
|
|
26
|
+
ActiveadminConfigurableColumns.preference_store.store(
|
|
27
|
+
admin: current_active_admin_user,
|
|
28
|
+
resource_key: registry.resource_key,
|
|
29
|
+
keys: registry.selected_keys(params[:visible_columns])
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
redirect_back fallback_location: page_path,
|
|
33
|
+
status: :see_other,
|
|
34
|
+
notice: t('active_admin.table_columns.saved')
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# A single column an admin may show or hide. Mirrors the arguments of
|
|
3
|
+
# ActiveAdmin's `column` so a declaration reads the same as the index DSL it
|
|
4
|
+
# replaces.
|
|
5
|
+
class Column
|
|
6
|
+
attr_reader :key, :attribute, :label, :options, :block
|
|
7
|
+
|
|
8
|
+
def initialize(key, label: nil, default: true, attribute: nil, block: nil, **options)
|
|
9
|
+
@key = key.to_sym
|
|
10
|
+
@attribute = (attribute || key).to_sym
|
|
11
|
+
@label = label
|
|
12
|
+
@default = default
|
|
13
|
+
@options = options
|
|
14
|
+
@block = block
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def default? = @default
|
|
18
|
+
|
|
19
|
+
# Emits the column into an ActiveAdmin index table.
|
|
20
|
+
def render_into(table)
|
|
21
|
+
args = []
|
|
22
|
+
args << label if label
|
|
23
|
+
args << attribute unless label && block
|
|
24
|
+
args << options
|
|
25
|
+
|
|
26
|
+
table.column(*args, &cell_renderer(table))
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# ActiveAdmin renders a column block with a plain `block.call(resource)`
|
|
30
|
+
# (DisplayHelper#find_value), so the block runs with whatever `self` it captured
|
|
31
|
+
# where it was written. Written inside an `index` block that is the view, which is
|
|
32
|
+
# how view helpers resolve. Ours is written inside a `configurable_columns` block,
|
|
33
|
+
# evaluated against the Builder, so it has to be rebound to the table at render
|
|
34
|
+
# time -- otherwise a helper call in a column block raises NoMethodError.
|
|
35
|
+
def cell_renderer(table)
|
|
36
|
+
return nil unless block
|
|
37
|
+
|
|
38
|
+
proc { |resource| table.instance_exec(resource, &block) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
# Human name for the checkbox in the settings form.
|
|
42
|
+
def human_label(resource_class)
|
|
43
|
+
return label if label
|
|
44
|
+
return attribute.to_s.humanize unless resource_class.respond_to?(:human_attribute_name)
|
|
45
|
+
|
|
46
|
+
resource_class.human_attribute_name(attribute, default: attribute.to_s.humanize)
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# Mixed into ActiveAdmin::ResourceDSL. Declares the columns an admin may pick
|
|
3
|
+
# from, and adds the "Columns" sidebar section to the index page:
|
|
4
|
+
#
|
|
5
|
+
# ActiveAdmin.register Product do
|
|
6
|
+
# configurable_columns do
|
|
7
|
+
# column :brand
|
|
8
|
+
# column :qty, sortable: 'stock_items_quantity' { |p| p.total_quantity }
|
|
9
|
+
# column :notes, default: false
|
|
10
|
+
# end
|
|
11
|
+
#
|
|
12
|
+
# index do
|
|
13
|
+
# selectable_column
|
|
14
|
+
# configurable_columns
|
|
15
|
+
# actions
|
|
16
|
+
# end
|
|
17
|
+
# end
|
|
18
|
+
module Dsl
|
|
19
|
+
def configurable_columns(&block)
|
|
20
|
+
config.column_registry = Registry.build(config.resource_name.route_key, &block)
|
|
21
|
+
config.sidebar_sections << columns_sidebar_section
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
private
|
|
25
|
+
|
|
26
|
+
def columns_sidebar_section
|
|
27
|
+
ActiveAdmin::SidebarSection.new(
|
|
28
|
+
:columns,
|
|
29
|
+
only: :index,
|
|
30
|
+
priority: ActiveadminConfigurableColumns.sidebar_priority
|
|
31
|
+
) do
|
|
32
|
+
render 'activeadmin_configurable_columns/sidebar', resource: active_admin_config
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# Wires the library into the host application: autoloads the Preference model,
|
|
3
|
+
# adds the gem's views and locales, and teaches ActiveAdmin the DSL before the
|
|
4
|
+
# application's app/admin files load (ActiveAdmin loads them lazily, long after
|
|
5
|
+
# the initializers ran).
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
engine_name 'activeadmin_configurable_columns'
|
|
8
|
+
|
|
9
|
+
initializer 'activeadmin_configurable_columns.active_admin' do
|
|
10
|
+
ActiveAdmin::Resource.include ResourceExtension
|
|
11
|
+
ActiveAdmin::ResourceDSL.include Dsl
|
|
12
|
+
ActiveAdmin::Views::IndexAsTable::IndexTableFor.include TableRenderer
|
|
13
|
+
|
|
14
|
+
# The "Table columns" page is a regular ActiveAdmin page registration, not
|
|
15
|
+
# a constant, so it lives outside the autoload paths and loads through
|
|
16
|
+
# ActiveAdmin's own load paths, together with the application's app/admin.
|
|
17
|
+
ActiveAdmin.application.load_paths <<
|
|
18
|
+
Engine.root.join('lib', 'activeadmin_configurable_columns', 'admin').to_s
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# The columns declared for one ActiveAdmin resource, in declaration order.
|
|
3
|
+
class Registry
|
|
4
|
+
attr_reader :resource_key, :columns
|
|
5
|
+
|
|
6
|
+
def self.build(resource_key, &block)
|
|
7
|
+
builder = Builder.new
|
|
8
|
+
builder.instance_eval(&block)
|
|
9
|
+
new(resource_key, builder.columns)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def initialize(resource_key, columns)
|
|
13
|
+
@resource_key = resource_key.to_s
|
|
14
|
+
@columns = columns.freeze
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def keys = columns.map(&:key)
|
|
18
|
+
|
|
19
|
+
def default_keys = columns.select(&:default?).map(&:key)
|
|
20
|
+
|
|
21
|
+
# Declared columns matching the given keys, in declaration order. Unknown or
|
|
22
|
+
# stale keys are dropped; a blank selection falls back to the defaults, so an
|
|
23
|
+
# admin can never end up with an empty table.
|
|
24
|
+
def columns_for(keys)
|
|
25
|
+
wanted = Array(keys).map(&:to_sym)
|
|
26
|
+
wanted = default_keys if (wanted & self.keys).empty?
|
|
27
|
+
|
|
28
|
+
columns.select { |column| wanted.include?(column.key) }
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def selected_keys(keys) = columns_for(keys).map(&:key)
|
|
32
|
+
|
|
33
|
+
# Collects `column` declarations from a `configurable_columns` block.
|
|
34
|
+
class Builder
|
|
35
|
+
attr_reader :columns
|
|
36
|
+
|
|
37
|
+
def initialize
|
|
38
|
+
@columns = []
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def column(key, **options, &block)
|
|
42
|
+
@columns << Column.new(key, block: block, **options)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# Mixed into ActiveAdmin::Resource so every registered resource can carry the
|
|
3
|
+
# registry declared for it. Resources are rebuilt whenever app/admin is
|
|
4
|
+
# reloaded, so the registry never outlives the code that declared it.
|
|
5
|
+
module ResourceExtension
|
|
6
|
+
attr_accessor :column_registry
|
|
7
|
+
|
|
8
|
+
def configurable_columns? = !column_registry.nil?
|
|
9
|
+
end
|
|
10
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
module ActiveadminConfigurableColumns
|
|
2
|
+
# Mixed into ActiveAdmin::Views::IndexAsTable::IndexTableFor, so that
|
|
3
|
+
# `configurable_columns` inside an `index` block renders the columns the
|
|
4
|
+
# current admin chose (falling back to the declared defaults).
|
|
5
|
+
module TableRenderer
|
|
6
|
+
def configurable_columns
|
|
7
|
+
registry = active_admin_config.column_registry
|
|
8
|
+
|
|
9
|
+
if registry.nil?
|
|
10
|
+
raise ActiveadminConfigurableColumns::NotDeclared, active_admin_config.resource_name.to_s
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
ActiveadminConfigurableColumns
|
|
14
|
+
.visible_columns(registry, helpers.current_active_admin_user)
|
|
15
|
+
.each { |column| column.render_into(self) }
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
require 'active_admin'
|
|
2
|
+
|
|
3
|
+
require_relative 'activeadmin_configurable_columns/version'
|
|
4
|
+
|
|
5
|
+
# Per-admin index table columns for ActiveAdmin.
|
|
6
|
+
#
|
|
7
|
+
# ActiveAdmin has no built-in way to let each admin choose which columns of an
|
|
8
|
+
# index table they see. This gem adds one, built on ActiveAdmin's own extension
|
|
9
|
+
# points:
|
|
10
|
+
#
|
|
11
|
+
# * ResourceDSL -- `configurable_columns` declares the available columns
|
|
12
|
+
# * IndexTableFor -- `configurable_columns` renders the chosen ones
|
|
13
|
+
# * SidebarSection -- the picker next to every configurable index table
|
|
14
|
+
#
|
|
15
|
+
# The choice is stored per admin by the preference store, which defaults to the
|
|
16
|
+
# bundled ActiveadminConfigurableColumns::Preference model.
|
|
17
|
+
module ActiveadminConfigurableColumns
|
|
18
|
+
# ActiveAdmin renders the filters sidebar section with priority 10; the
|
|
19
|
+
# default puts the "Columns" picker right below it.
|
|
20
|
+
DEFAULT_SIDEBAR_PRIORITY = 20
|
|
21
|
+
|
|
22
|
+
# Raised when an index renders `configurable_columns` without a matching
|
|
23
|
+
# `configurable_columns do ... end` declaration.
|
|
24
|
+
class NotDeclared < StandardError
|
|
25
|
+
def initialize(resource_name)
|
|
26
|
+
super("#{resource_name} renders configurable_columns but never declares them. " \
|
|
27
|
+
'Add a `configurable_columns do ... end` block to its ActiveAdmin registration.')
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
class << self
|
|
32
|
+
def configure
|
|
33
|
+
yield self
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
# Where saved selections live. Any object answering
|
|
37
|
+
#
|
|
38
|
+
# visible_column_keys(admin, resource_key) -> [Symbol] | nil (nil = never saved)
|
|
39
|
+
# store(admin:, resource_key:, keys:) (persists a selection)
|
|
40
|
+
#
|
|
41
|
+
# can be plugged in; the default is the bundled Preference model. Assigning
|
|
42
|
+
# nil restores that default.
|
|
43
|
+
attr_writer :preference_store
|
|
44
|
+
|
|
45
|
+
def preference_store = @preference_store || Preference
|
|
46
|
+
|
|
47
|
+
# Priority of the "Columns" sidebar section. Assigning nil restores the default.
|
|
48
|
+
attr_writer :sidebar_priority
|
|
49
|
+
|
|
50
|
+
def sidebar_priority = @sidebar_priority || DEFAULT_SIDEBAR_PRIORITY
|
|
51
|
+
|
|
52
|
+
# Options passed to `menu` for the "Table columns" page (assign false to keep
|
|
53
|
+
# the page out of the menu entirely). Assigning nil restores the default.
|
|
54
|
+
attr_writer :menu
|
|
55
|
+
|
|
56
|
+
def menu
|
|
57
|
+
return @menu unless @menu.nil?
|
|
58
|
+
|
|
59
|
+
{ label: proc { I18n.t('active_admin.table_columns.title') } }
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# The columns the given admin sees for this resource.
|
|
63
|
+
def visible_columns(registry, admin)
|
|
64
|
+
saved = preference_store.visible_column_keys(admin, registry.resource_key)
|
|
65
|
+
|
|
66
|
+
registry.columns_for(saved || registry.default_keys)
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Every resource of the namespace that declares configurable columns.
|
|
70
|
+
def resources(namespace)
|
|
71
|
+
namespace.resources.select do |resource|
|
|
72
|
+
resource.respond_to?(:configurable_columns?) && resource.configurable_columns?
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def resource_for(namespace, resource_key)
|
|
77
|
+
resources(namespace).find { |resource| resource.column_registry.resource_key == resource_key.to_s }
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Name of a route helper for the "Table columns" page in the given namespace:
|
|
81
|
+
#
|
|
82
|
+
# route_name(admin_namespace) # => 'admin_table_columns_path'
|
|
83
|
+
# route_name(admin_namespace, :update) # => 'admin_table_columns_update_path'
|
|
84
|
+
#
|
|
85
|
+
# The namespace prefix is looked up rather than hardcoded, so the page works
|
|
86
|
+
# in any ActiveAdmin namespace, including the root one.
|
|
87
|
+
def route_name(namespace, action = nil)
|
|
88
|
+
[namespace.route_prefix, 'table_columns', action, 'path'].compact.join('_')
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
require_relative 'activeadmin_configurable_columns/column'
|
|
94
|
+
require_relative 'activeadmin_configurable_columns/registry'
|
|
95
|
+
require_relative 'activeadmin_configurable_columns/dsl'
|
|
96
|
+
require_relative 'activeadmin_configurable_columns/resource_extension'
|
|
97
|
+
require_relative 'activeadmin_configurable_columns/table_renderer'
|
|
98
|
+
require_relative 'activeadmin_configurable_columns/engine'
|
metadata
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: activeadmin_configurable_columns
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 1.0.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Kyryl Oliinyk
|
|
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: 4.0.0.beta15
|
|
19
|
+
- - "<"
|
|
20
|
+
- !ruby/object:Gem::Version
|
|
21
|
+
version: '4.1'
|
|
22
|
+
type: :runtime
|
|
23
|
+
prerelease: false
|
|
24
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
25
|
+
requirements:
|
|
26
|
+
- - ">="
|
|
27
|
+
- !ruby/object:Gem::Version
|
|
28
|
+
version: 4.0.0.beta15
|
|
29
|
+
- - "<"
|
|
30
|
+
- !ruby/object:Gem::Version
|
|
31
|
+
version: '4.1'
|
|
32
|
+
- !ruby/object:Gem::Dependency
|
|
33
|
+
name: activerecord
|
|
34
|
+
requirement: !ruby/object:Gem::Requirement
|
|
35
|
+
requirements:
|
|
36
|
+
- - ">="
|
|
37
|
+
- !ruby/object:Gem::Version
|
|
38
|
+
version: '7.1'
|
|
39
|
+
type: :runtime
|
|
40
|
+
prerelease: false
|
|
41
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
42
|
+
requirements:
|
|
43
|
+
- - ">="
|
|
44
|
+
- !ruby/object:Gem::Version
|
|
45
|
+
version: '7.1'
|
|
46
|
+
- !ruby/object:Gem::Dependency
|
|
47
|
+
name: railties
|
|
48
|
+
requirement: !ruby/object:Gem::Requirement
|
|
49
|
+
requirements:
|
|
50
|
+
- - ">="
|
|
51
|
+
- !ruby/object:Gem::Version
|
|
52
|
+
version: '7.1'
|
|
53
|
+
type: :runtime
|
|
54
|
+
prerelease: false
|
|
55
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
56
|
+
requirements:
|
|
57
|
+
- - ">="
|
|
58
|
+
- !ruby/object:Gem::Version
|
|
59
|
+
version: '7.1'
|
|
60
|
+
description: Lets every admin choose which columns of an ActiveAdmin index table they
|
|
61
|
+
see. Adds a `configurable_columns` DSL to declare the available columns, renders
|
|
62
|
+
a "Columns" picker in the index sidebar and on a central "Table columns" page, and
|
|
63
|
+
stores each admin's choice in the database.
|
|
64
|
+
email:
|
|
65
|
+
- kyryloliinyk@gmail.com
|
|
66
|
+
executables: []
|
|
67
|
+
extensions: []
|
|
68
|
+
extra_rdoc_files: []
|
|
69
|
+
files:
|
|
70
|
+
- CHANGELOG.md
|
|
71
|
+
- LICENSE.txt
|
|
72
|
+
- README.md
|
|
73
|
+
- app/models/activeadmin_configurable_columns/preference.rb
|
|
74
|
+
- app/views/activeadmin_configurable_columns/_form.html.erb
|
|
75
|
+
- app/views/activeadmin_configurable_columns/_page.html.erb
|
|
76
|
+
- app/views/activeadmin_configurable_columns/_sidebar.html.erb
|
|
77
|
+
- config/locales/en.yml
|
|
78
|
+
- db/migrate/20260930000001_create_activeadmin_configurable_columns_preferences.rb
|
|
79
|
+
- lib/activeadmin_configurable_columns.rb
|
|
80
|
+
- lib/activeadmin_configurable_columns/admin/table_columns.rb
|
|
81
|
+
- lib/activeadmin_configurable_columns/column.rb
|
|
82
|
+
- lib/activeadmin_configurable_columns/dsl.rb
|
|
83
|
+
- lib/activeadmin_configurable_columns/engine.rb
|
|
84
|
+
- lib/activeadmin_configurable_columns/registry.rb
|
|
85
|
+
- lib/activeadmin_configurable_columns/resource_extension.rb
|
|
86
|
+
- lib/activeadmin_configurable_columns/table_renderer.rb
|
|
87
|
+
- lib/activeadmin_configurable_columns/version.rb
|
|
88
|
+
homepage: https://github.com/kyryloliinyk/activeadmin_configurable_columns
|
|
89
|
+
licenses:
|
|
90
|
+
- MIT
|
|
91
|
+
metadata:
|
|
92
|
+
source_code_uri: https://github.com/kyryloliinyk/activeadmin_configurable_columns
|
|
93
|
+
changelog_uri: https://github.com/kyryloliinyk/activeadmin_configurable_columns/blob/main/CHANGELOG.md
|
|
94
|
+
bug_tracker_uri: https://github.com/kyryloliinyk/activeadmin_configurable_columns/issues
|
|
95
|
+
rubygems_mfa_required: 'true'
|
|
96
|
+
rdoc_options: []
|
|
97
|
+
require_paths:
|
|
98
|
+
- lib
|
|
99
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '3.1'
|
|
104
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
105
|
+
requirements:
|
|
106
|
+
- - ">="
|
|
107
|
+
- !ruby/object:Gem::Version
|
|
108
|
+
version: '0'
|
|
109
|
+
requirements: []
|
|
110
|
+
rubygems_version: 4.0.19
|
|
111
|
+
specification_version: 4
|
|
112
|
+
summary: Per-admin configurable index table columns for ActiveAdmin 4.
|
|
113
|
+
test_files: []
|