infotable 0.1.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/MIT-LICENSE +20 -0
- data/README.md +1043 -0
- data/Rakefile +6 -0
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +1412 -0
- data/app/assets/javascripts/infotable/controllers/table_controller.js +921 -0
- data/app/assets/javascripts/infotable/index.js +14 -0
- data/app/controllers/infotable/application_controller.rb +4 -0
- data/app/controllers/infotable/tables_controller.rb +299 -0
- data/app/helpers/infotable/application_helper.rb +5 -0
- data/app/helpers/infotable/tables_helper.rb +354 -0
- data/app/jobs/infotable/application_job.rb +4 -0
- data/app/mailers/infotable/application_mailer.rb +6 -0
- data/app/models/infotable/application_record.rb +5 -0
- data/app/views/infotable/tables/_column_visibility.html.erb +100 -0
- data/app/views/infotable/tables/_filter_drawer.html.erb +65 -0
- data/app/views/infotable/tables/_next_page_frame.html.erb +39 -0
- data/app/views/infotable/tables/_pagination.html.erb +101 -0
- data/app/views/infotable/tables/_side_panel.html.erb +63 -0
- data/app/views/infotable/tables/_table.html.erb +135 -0
- data/app/views/infotable/tables/_table_rows.html.erb +38 -0
- data/app/views/infotable/tables/filters/_boolean.html.erb +22 -0
- data/app/views/infotable/tables/filters/_date.html.erb +30 -0
- data/app/views/infotable/tables/filters/_number.html.erb +32 -0
- data/app/views/infotable/tables/filters/_select.html.erb +68 -0
- data/app/views/infotable/tables/filters/_text.html.erb +38 -0
- data/app/views/infotable/tables/show.html.erb +113 -0
- data/app/views/layouts/infotable/application.html.erb +15 -0
- data/config/importmap.rb +6 -0
- data/config/locales/en.yml +88 -0
- data/config/locales/pl.yml +88 -0
- data/config/routes.rb +6 -0
- data/lib/generators/infotable/install/install_generator.rb +565 -0
- data/lib/generators/infotable/install/templates/TAILWIND_CONFIG.md +131 -0
- data/lib/generators/infotable/install/templates/initializer.rb +26 -0
- data/lib/generators/infotable/install/templates/tailwind.config.js +284 -0
- data/lib/generators/infotable/table/table_generator.rb +95 -0
- data/lib/generators/infotable/table/templates/table.rb.tt +26 -0
- data/lib/infotable/base.rb +463 -0
- data/lib/infotable/batch_actions.rb +34 -0
- data/lib/infotable/configuration.rb +53 -0
- data/lib/infotable/engine.rb +51 -0
- data/lib/infotable/filter_processor.rb +326 -0
- data/lib/infotable/formatters/action_formatter.rb +141 -0
- data/lib/infotable/formatters/avatar_formatter.rb +49 -0
- data/lib/infotable/formatters/badge_formatter.rb +80 -0
- data/lib/infotable/formatters/base_formatter.rb +47 -0
- data/lib/infotable/formatters/boolean_formatter.rb +38 -0
- data/lib/infotable/formatters/currency_formatter.rb +22 -0
- data/lib/infotable/formatters/date_formatter.rb +58 -0
- data/lib/infotable/formatters/link_formatter.rb +48 -0
- data/lib/infotable/formatters/percent_formatter.rb +27 -0
- data/lib/infotable/search_processor.rb +42 -0
- data/lib/infotable/table_resolver.rb +45 -0
- data/lib/infotable/version.rb +3 -0
- data/lib/infotable.rb +54 -0
- data/lib/tasks/infotable.rake +55 -0
- metadata +216 -0
data/README.md
ADDED
|
@@ -0,0 +1,1043 @@
|
|
|
1
|
+
# InfoTable
|
|
2
|
+
|
|
3
|
+
Modern, flexible datatable engine for Rails 8+ with Hotwire. Build beautiful, interactive tables with server-side filtering, sorting, searching, and pagination—without page reloads.
|
|
4
|
+
|
|
5
|
+
## Features
|
|
6
|
+
|
|
7
|
+
- ✨ **Server-side operations** - Filtering, sorting, searching, pagination all on the backend
|
|
8
|
+
- ⚡ **No page reloads** - Built with Hotwire (Turbo + Stimulus)
|
|
9
|
+
- 🎨 **Pure Tailwind CSS** - Beautiful, modern UI using only Tailwind utility classes (no custom CSS!)
|
|
10
|
+
- 🔧 **Highly customizable** - Easy to override defaults and add custom formatters
|
|
11
|
+
- 📦 **Works with any bundler** - Importmap, esbuild, webpack, vite—we support them all
|
|
12
|
+
- 🚀 **Rails 8+ ready** - Built for modern Rails applications
|
|
13
|
+
- 🌍 **i18n Support** - English and Polish translations included
|
|
14
|
+
- 🎯 **Advanced Features** - Infinite scroll, column pinning, drag-to-reorder, row links, and more
|
|
15
|
+
|
|
16
|
+
## Quick Start
|
|
17
|
+
|
|
18
|
+
```ruby
|
|
19
|
+
# 1. Add to Gemfile
|
|
20
|
+
gem 'infotable'
|
|
21
|
+
|
|
22
|
+
# 2. Install
|
|
23
|
+
bundle install
|
|
24
|
+
rails generate infotable:install
|
|
25
|
+
|
|
26
|
+
# 3. Create a table definition (app/infotable/users_table.rb)
|
|
27
|
+
class UsersTable < Infotable::Base
|
|
28
|
+
query { User.all }
|
|
29
|
+
|
|
30
|
+
column :name, label: "Name", type: :string, sortable: true
|
|
31
|
+
column :email, label: "Email", type: :string, sortable: true
|
|
32
|
+
column :created_at, label: "Created", type: :datetime, sortable: true
|
|
33
|
+
|
|
34
|
+
per_page 25
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# 4. Use in your view
|
|
38
|
+
<%= infotable :users_table %>
|
|
39
|
+
```
|
|
40
|
+
|
|
41
|
+
That's it! You now have a fully functional datatable with sorting and pagination.
|
|
42
|
+
|
|
43
|
+
## Requirements
|
|
44
|
+
|
|
45
|
+
- Rails 8.0+
|
|
46
|
+
- Ruby 3.3+
|
|
47
|
+
- PostgreSQL (filters use `ILIKE`, `HAVING` and `CAST(... AS numeric)`; other databases are not supported)
|
|
48
|
+
- [Pagy](https://github.com/ddnexus/pagy) 43 (installed as a dependency)
|
|
49
|
+
- Tailwind CSS v4 (via `tailwindcss-rails` gem) - **Required for styling**
|
|
50
|
+
- Hotwire (Turbo + Stimulus)
|
|
51
|
+
|
|
52
|
+
**Note:** InfoTable uses pure Tailwind CSS utility classes for all styling. No custom CSS is included or required.
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
### Automatic Installation (Recommended)
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
bundle add infotable
|
|
60
|
+
rails generate infotable:install
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The installer will:
|
|
64
|
+
- Auto-detect your JavaScript bundler (importmap, esbuild, webpack, vite)
|
|
65
|
+
- Configure Tailwind CSS (v4 or v3) to scan InfoTable views
|
|
66
|
+
- Mount engine routes
|
|
67
|
+
- Create `app/infotable/` directory
|
|
68
|
+
- Generate configuration file
|
|
69
|
+
|
|
70
|
+
**After installation:**
|
|
71
|
+
```bash
|
|
72
|
+
# Rebuild Tailwind to include InfoTable styles
|
|
73
|
+
rails tailwindcss:build
|
|
74
|
+
|
|
75
|
+
# Restart your Rails server
|
|
76
|
+
rails server
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
**Important:** Verify your layout (`app/views/layouts/application.html.erb`) includes:
|
|
80
|
+
```erb
|
|
81
|
+
<%= stylesheet_link_tag "tailwind", "data-turbo-track": "reload" %>
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### Manual Installation
|
|
85
|
+
|
|
86
|
+
See [docs/installation.md](docs/installation.md) for detailed instructions for each bundler type.
|
|
87
|
+
|
|
88
|
+
## Bundler Support
|
|
89
|
+
|
|
90
|
+
InfoTable works seamlessly with:
|
|
91
|
+
- **Importmap** (Rails default)
|
|
92
|
+
- **esbuild**
|
|
93
|
+
- **webpack**
|
|
94
|
+
- **vite**
|
|
95
|
+
- Other JavaScript bundlers
|
|
96
|
+
|
|
97
|
+
The installer automatically detects and configures your setup.
|
|
98
|
+
|
|
99
|
+
---
|
|
100
|
+
|
|
101
|
+
## Configuration
|
|
102
|
+
|
|
103
|
+
### Global Configuration
|
|
104
|
+
|
|
105
|
+
Customize in `config/initializers/infotable.rb`:
|
|
106
|
+
|
|
107
|
+
```ruby
|
|
108
|
+
Infotable.configure do |config|
|
|
109
|
+
# Pagination defaults
|
|
110
|
+
config.default_per_page = 25
|
|
111
|
+
config.per_page_options = [10, 25, 50, 100]
|
|
112
|
+
|
|
113
|
+
# Drawer/side panel settings
|
|
114
|
+
config.drawer_position = :right # :left or :right
|
|
115
|
+
config.drawer_width = "26rem" # px, rem, em, vw or %
|
|
116
|
+
|
|
117
|
+
# Controller the engine controllers inherit from, so authentication, locale,
|
|
118
|
+
# tenant scoping etc. also protect the tables
|
|
119
|
+
config.parent_controller = "ApplicationController"
|
|
120
|
+
|
|
121
|
+
# Table height defaults
|
|
122
|
+
config.table_min_height = "400px"
|
|
123
|
+
config.table_max_height = "600px"
|
|
124
|
+
|
|
125
|
+
# Filter behavior
|
|
126
|
+
config.autofilter = true # true = auto-apply filters (debounced), false = manual apply button
|
|
127
|
+
end
|
|
128
|
+
```
|
|
129
|
+
|
|
130
|
+
### Available Global Options
|
|
131
|
+
|
|
132
|
+
| Option | Type | Default | Description |
|
|
133
|
+
|--------|------|---------|-------------|
|
|
134
|
+
| `default_per_page` | Integer | `25` | Default number of rows per page |
|
|
135
|
+
| `per_page_options` | Array | `[10, 25, 50, 100]` | Available rows per page options |
|
|
136
|
+
| `drawer_position` | Symbol | `:right` | Position of filter/column drawer (`:left` or `:right`) |
|
|
137
|
+
| `drawer_width` | String | `"26rem"` | Width of the drawer (`px`, `rem`, `em`, `vw` or `%`; anything else falls back to the default) |
|
|
138
|
+
| `parent_controller` | String | `"ActionController::Base"` | Controller the engine's controllers inherit from. Set it to `"ApplicationController"` to reuse your authentication |
|
|
139
|
+
| `table_min_height` | String | `"400px"` | Minimum table height |
|
|
140
|
+
| `table_max_height` | String | `"600px"` | Maximum table height (enables scrolling) |
|
|
141
|
+
| `autofilter` | Boolean | `true` | Auto-apply filters on change (debounced) vs manual apply |
|
|
142
|
+
| `url_state` | Boolean | `true` | Mirror search, filters, sort and page in the address bar (override per table with `url_state false`) |
|
|
143
|
+
| `max_filter_options` | Integer | `500` | Cap on `<option>`s in a select filter (`nil` = no cap) |
|
|
144
|
+
|
|
145
|
+
`theme`, `turbo_enabled` and `pagination_gem` are still accepted so old initializers keep loading, but they do nothing.
|
|
146
|
+
|
|
147
|
+
---
|
|
148
|
+
|
|
149
|
+
## Table Definition
|
|
150
|
+
|
|
151
|
+
Generate a starting point from a model's columns:
|
|
152
|
+
|
|
153
|
+
```bash
|
|
154
|
+
rails generate infotable:table User
|
|
155
|
+
rails generate infotable:table Admin::Order --only=id number status # or --skip=notes
|
|
156
|
+
```
|
|
157
|
+
|
|
158
|
+
It writes `app/infotable/users_table.rb` with a column per attribute (types and filters inferred, enums become select filters, passwords/tokens and foreign keys are left out, `belongs_to` associations come as commented hints).
|
|
159
|
+
|
|
160
|
+
Or create table classes in `app/infotable/` by hand:
|
|
161
|
+
|
|
162
|
+
```ruby
|
|
163
|
+
class UsersTable < Infotable::Base
|
|
164
|
+
# Define data source
|
|
165
|
+
query { User.all }
|
|
166
|
+
|
|
167
|
+
# Define columns
|
|
168
|
+
column :name, label: "Full Name", type: :string, sortable: true
|
|
169
|
+
|
|
170
|
+
# Configure table settings
|
|
171
|
+
per_page 25
|
|
172
|
+
default_sort :created_at, :desc
|
|
173
|
+
end
|
|
174
|
+
```
|
|
175
|
+
|
|
176
|
+
### Organizing Tables in Folders
|
|
177
|
+
|
|
178
|
+
You can organize table definitions in subfolders. Rails' Zeitwerk autoloader automatically creates namespaces from the directory structure:
|
|
179
|
+
|
|
180
|
+
**Single-level nesting:**
|
|
181
|
+
```ruby
|
|
182
|
+
# app/infotable/cms/users_table.rb
|
|
183
|
+
class Cms::UsersTable < Infotable::Base
|
|
184
|
+
query { User.all }
|
|
185
|
+
# ... columns
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Use in views:
|
|
189
|
+
<%= infotable :cms_users_table %>
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
**Multi-level nesting:**
|
|
193
|
+
```ruby
|
|
194
|
+
# app/infotable/cms/admins/users_table.rb
|
|
195
|
+
class Cms::Admins::UsersTable < Infotable::Base
|
|
196
|
+
query { User.where(admin: true) }
|
|
197
|
+
# ... columns
|
|
198
|
+
end
|
|
199
|
+
|
|
200
|
+
# Use in views:
|
|
201
|
+
<%= infotable :cms_admins_users_table %>
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
**How it works:**
|
|
205
|
+
- Rails' Zeitwerk autoloader infers namespaces from directory structure
|
|
206
|
+
- File path: `app/infotable/cms/users_table.rb` → Class: `Cms::UsersTable` → Symbol: `:cms_users_table`
|
|
207
|
+
- File path: `app/infotable/cms/admins/users_table.rb` → Class: `Cms::Admins::UsersTable` → Symbol: `:cms_admins_users_table`
|
|
208
|
+
- No need for `module` wrappers - the directory structure provides the namespace
|
|
209
|
+
|
|
210
|
+
**Naming convention:**
|
|
211
|
+
- Pattern: folder names joined with underscores, followed by the table name
|
|
212
|
+
- Example: folder `cms/admins` + file `users_table.rb` = symbol `:cms_admins_users_table`
|
|
213
|
+
|
|
214
|
+
### Data Source Methods
|
|
215
|
+
|
|
216
|
+
#### `query`
|
|
217
|
+
Define data source using ActiveRecord relation:
|
|
218
|
+
|
|
219
|
+
```ruby
|
|
220
|
+
query { User.includes(:company).where(active: true) }
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
The block receives the controller, so you can scope by the current user:
|
|
224
|
+
|
|
225
|
+
```ruby
|
|
226
|
+
query { |controller| controller.current_user.projects }
|
|
227
|
+
```
|
|
228
|
+
|
|
229
|
+
#### `query_sql`
|
|
230
|
+
Define data source using raw SQL. A `model:` is required; it supplies the column types and the table alias. The SQL is wrapped in a sub-select, so filters, search, sorting, pagination and export all work:
|
|
231
|
+
|
|
232
|
+
```ruby
|
|
233
|
+
query_sql "SELECT * FROM users WHERE active = true", model: User
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
#### `authorize`
|
|
237
|
+
Return false to answer `403 Forbidden` for the table, its data and its export:
|
|
238
|
+
|
|
239
|
+
```ruby
|
|
240
|
+
authorize { |controller| controller.current_user&.admin? }
|
|
241
|
+
```
|
|
242
|
+
|
|
243
|
+
### Column Definition
|
|
244
|
+
|
|
245
|
+
```ruby
|
|
246
|
+
column :name, **options
|
|
247
|
+
```
|
|
248
|
+
|
|
249
|
+
#### Virtual/Computed Columns
|
|
250
|
+
|
|
251
|
+
For aggregated or computed columns (COUNT, SUM, subqueries, calculations), mark them as `virtual: true`:
|
|
252
|
+
|
|
253
|
+
```ruby
|
|
254
|
+
query do
|
|
255
|
+
Shop.select(
|
|
256
|
+
"shops.*",
|
|
257
|
+
"COUNT(products.id) AS products_count"
|
|
258
|
+
)
|
|
259
|
+
.left_joins(:products)
|
|
260
|
+
.group("shops.id")
|
|
261
|
+
end
|
|
262
|
+
|
|
263
|
+
# Regular column
|
|
264
|
+
column :name, sortable: true, filterable: true
|
|
265
|
+
|
|
266
|
+
# Virtual column (aggregate)
|
|
267
|
+
column :products_count,
|
|
268
|
+
type: :integer,
|
|
269
|
+
sortable: true, # ✅ Sorting works
|
|
270
|
+
filterable: true, # ✅ Filtering uses HAVING clause
|
|
271
|
+
filter_type: :number,
|
|
272
|
+
virtual: true, # ⭐ Required for aggregates!
|
|
273
|
+
virtual_sql: "COUNT(products.id)" # ⭐ SQL expression for filtering (without AS alias)
|
|
274
|
+
```
|
|
275
|
+
|
|
276
|
+
**Why `virtual: true` and `virtual_sql`?**
|
|
277
|
+
- Aggregated columns require `HAVING` clause for filtering (not `WHERE`)
|
|
278
|
+
- The `virtual_sql` option provides the SQL expression used in both HAVING and ORDER BY clauses
|
|
279
|
+
- InfoTable automatically uses the correct SQL clauses for filtering and sorting
|
|
280
|
+
- Works with all filter types
|
|
281
|
+
- See [docs/virtual_columns.md](docs/virtual_columns.md) for detailed examples
|
|
282
|
+
|
|
283
|
+
#### Column Options
|
|
284
|
+
|
|
285
|
+
| Option | Type | Default | Description |
|
|
286
|
+
|--------|------|---------|-------------|
|
|
287
|
+
| `label` | String | Humanized column name | Display label for column header |
|
|
288
|
+
| `type` | Symbol | `:string` | Data type (see Column Types below) |
|
|
289
|
+
| `sortable` | Boolean | `false` | Enable sorting for this column |
|
|
290
|
+
| `searchable` | Boolean | `false` | Include in global search |
|
|
291
|
+
| `filterable` | Boolean | `false` | Enable filtering for this column |
|
|
292
|
+
| `visible` | Boolean | `true` | Show column by default |
|
|
293
|
+
| `pinned` | Symbol | `nil` | Pin column (`:left`, `:right`, or `nil`) |
|
|
294
|
+
| `width` | String | Auto (see defaults) | Column width (e.g., `"200px"`) |
|
|
295
|
+
| `formatter` | Symbol/Proc/Hash | `nil` | Custom formatter (see Formatters) |
|
|
296
|
+
| `filter_type` | Symbol | Auto-detected | Filter UI type (`:text`, `:number`, `:date`, `:select`, `:boolean`) |
|
|
297
|
+
| `filter_options` | Array/Proc | `nil` | Options for select filters (`["a", "b"]`, `[["Label", value]]` or a Proc) |
|
|
298
|
+
| `filter_options_cache` | Duration | `nil` | Cache a Proc's result with `Rails.cache`, e.g. `5.minutes`. Keyed per table and column, so do not use it for options that depend on the current user |
|
|
299
|
+
| `filter_options_limit` | Integer | `max_filter_options` | Most `<option>`s rendered for this filter; the drawer says when the list was cut. For big lists use a text filter instead |
|
|
300
|
+
| `virtual` | Boolean | `false` | Mark column as virtual/computed (aggregates, subqueries) - uses HAVING for filters |
|
|
301
|
+
| `virtual_sql` | String | `nil` | SQL expression for virtual columns (used in HAVING and ORDER BY clauses, without AS alias) |
|
|
302
|
+
| `sql` | String | `nil` | Row-level SQL expression to filter and search on instead of the column (e.g. `"comments.post_id"`). Not an aggregate, so it goes in `WHERE` |
|
|
303
|
+
| `sort_sql` | String | `nil` | SQL expression to sort by (e.g. `"authors.name"` for an association column) |
|
|
304
|
+
| `value` | Proc | `nil` | Computes the cell value from the record: `value: ->(post) { post.author.name }` |
|
|
305
|
+
| `includes` | Symbol/Array/Hash | `nil` | Association to preload for this column (avoids N+1 in the table and the CSV export) |
|
|
306
|
+
| `filter_multi_select` | Boolean | `false` | Let a `:select` filter take several values |
|
|
307
|
+
|
|
308
|
+
#### Column Types
|
|
309
|
+
|
|
310
|
+
Available types and their default widths:
|
|
311
|
+
|
|
312
|
+
| Type | Default Width | Description |
|
|
313
|
+
|------|---------------|-------------|
|
|
314
|
+
| `:string` | `200px` | Text string |
|
|
315
|
+
| `:text` | `300px` | Long text content |
|
|
316
|
+
| `:integer` | `100px` | Integer number |
|
|
317
|
+
| `:decimal` | `120px` | Decimal number |
|
|
318
|
+
| `:float` | `120px` | Float number |
|
|
319
|
+
| `:currency` | `120px` | Currency value |
|
|
320
|
+
| `:boolean` | `80px` | True/false value |
|
|
321
|
+
| `:date` | `120px` | Date only |
|
|
322
|
+
| `:datetime` | `180px` | Date and time |
|
|
323
|
+
| `:badge` | `120px` | Badge/status indicator |
|
|
324
|
+
| `:status` | `120px` | Alias for badge |
|
|
325
|
+
| `:action` | `100px` | Action buttons |
|
|
326
|
+
| `:actions` | `100px` | Alias for action |
|
|
327
|
+
|
|
328
|
+
### Table Configuration Methods
|
|
329
|
+
|
|
330
|
+
#### Pagination
|
|
331
|
+
|
|
332
|
+
```ruby
|
|
333
|
+
# Set rows per page
|
|
334
|
+
per_page 25
|
|
335
|
+
|
|
336
|
+
# Set per-page options dropdown
|
|
337
|
+
per_page_options [10, 25, 50, 100]
|
|
338
|
+
```
|
|
339
|
+
|
|
340
|
+
#### Sorting
|
|
341
|
+
|
|
342
|
+
```ruby
|
|
343
|
+
# Set default sort
|
|
344
|
+
default_sort :created_at, :desc # column, direction (:asc or :desc)
|
|
345
|
+
```
|
|
346
|
+
|
|
347
|
+
Click a header to sort; **Shift+click** adds up to three sort columns (`?sort=name:asc,age:desc`). The primary key is always appended as a tiebreaker, so pages never repeat or skip rows.
|
|
348
|
+
|
|
349
|
+
#### Search
|
|
350
|
+
|
|
351
|
+
Mark columns `searchable: true` to get a search box. Every word (up to 5) must match at least one searchable column, case-insensitively.
|
|
352
|
+
|
|
353
|
+
#### Filters
|
|
354
|
+
|
|
355
|
+
Filter types are inferred from the column type or set with `filter_type:`.
|
|
356
|
+
|
|
357
|
+
| Type | Operators |
|
|
358
|
+
|------|-----------|
|
|
359
|
+
| text | contains, equals, starts with, ends with, not contains, is empty, is not empty |
|
|
360
|
+
| number | `eq` `neq` `gt` `gte` `lt` `lte` `between` `is_empty` `is_not_empty` (plain `from`/`to` still work) |
|
|
361
|
+
| date | `on` `before` `after` `between` `is_empty` `is_not_empty` (plain `from`/`to` still work) |
|
|
362
|
+
| boolean | any, yes, no |
|
|
363
|
+
| select | one value, or several with `filter_multi_select: true` |
|
|
364
|
+
|
|
365
|
+
Filters, search and sort survive pagination, per-page changes and CSV export.
|
|
366
|
+
|
|
367
|
+
#### Table state in the URL
|
|
368
|
+
|
|
369
|
+
Search, filters, sort, page and page size are mirrored in the page's address bar, one namespace per table (`/users?users_table[search]=ann&users_table[page]=2`). Links can be shared and bookmarked, a reload keeps the table as it was, and the Back button steps through the states. Turn it off globally with `config.url_state = false` or per table with `url_state false` (e.g. tables inside modals or tabs).
|
|
370
|
+
|
|
371
|
+
#### Large tables
|
|
372
|
+
|
|
373
|
+
```ruby
|
|
374
|
+
# Skip SELECT COUNT(*): only previous/next, no total or page numbers
|
|
375
|
+
countless
|
|
376
|
+
```
|
|
377
|
+
|
|
378
|
+
#### Batch actions
|
|
379
|
+
|
|
380
|
+
Row checkboxes plus an action bar. The gem renders them and sends the selected ids as `ids[]` to your endpoints:
|
|
381
|
+
|
|
382
|
+
```ruby
|
|
383
|
+
batch_actions do
|
|
384
|
+
action :feature, label: "Feature", url: "/posts/batch", method: :patch, color: :primary
|
|
385
|
+
action :delete, label: "Delete", url: "/posts/batch", method: :delete, color: :danger,
|
|
386
|
+
confirm: "Delete %{count} posts?"
|
|
387
|
+
end
|
|
388
|
+
|
|
389
|
+
# Checkboxes only (read the selection from your own JavaScript)
|
|
390
|
+
checkbox_column
|
|
391
|
+
```
|
|
392
|
+
|
|
393
|
+
`color:` is one of `:default`, `:primary`, `:success`, `:warning`, `:danger`; `method:` is one of `:get`, `:post`, `:put`, `:patch`, `:delete`.
|
|
394
|
+
|
|
395
|
+
#### Table Height
|
|
396
|
+
|
|
397
|
+
```ruby
|
|
398
|
+
# Override global height settings for this table
|
|
399
|
+
table_min_height "500px"
|
|
400
|
+
table_max_height "700px"
|
|
401
|
+
```
|
|
402
|
+
|
|
403
|
+
#### Infinite Scroll
|
|
404
|
+
|
|
405
|
+
```ruby
|
|
406
|
+
# Enable infinite scroll (replaces pagination)
|
|
407
|
+
infinite_scroll true
|
|
408
|
+
```
|
|
409
|
+
|
|
410
|
+
#### Row Links
|
|
411
|
+
|
|
412
|
+
Make entire rows clickable:
|
|
413
|
+
|
|
414
|
+
```ruby
|
|
415
|
+
# Dynamic URL based on record
|
|
416
|
+
row_link ->(record) { "/users/#{record.id}" }
|
|
417
|
+
|
|
418
|
+
# Static URL
|
|
419
|
+
row_link "/users/show"
|
|
420
|
+
```
|
|
421
|
+
|
|
422
|
+
Features:
|
|
423
|
+
- Click row to navigate
|
|
424
|
+
- Cmd/Ctrl+Click to open in new tab
|
|
425
|
+
- Ignores clicks on buttons, links, and inputs
|
|
426
|
+
|
|
427
|
+
#### Row Size
|
|
428
|
+
|
|
429
|
+
Control table row padding and text size with three options:
|
|
430
|
+
|
|
431
|
+
```ruby
|
|
432
|
+
# Slim - Compact padding (px-2 py-1) with smaller text
|
|
433
|
+
row_size :slim
|
|
434
|
+
|
|
435
|
+
# Mid - Medium padding (px-4 py-2.5) - default text size
|
|
436
|
+
row_size :mid
|
|
437
|
+
|
|
438
|
+
# Default - Full padding (px-6 py-4) with normal text (default)
|
|
439
|
+
row_size :default
|
|
440
|
+
```
|
|
441
|
+
|
|
442
|
+
**Backward compatibility:** The old `slim_rows true` method still works and maps to `row_size :slim`
|
|
443
|
+
|
|
444
|
+
**Visual comparison:**
|
|
445
|
+
- **Slim**: Minimal padding, smaller text - ideal for dense data tables
|
|
446
|
+
- **Mid**: Moderate padding, normal text - balanced appearance
|
|
447
|
+
- **Default**: Generous padding, normal text - comfortable reading
|
|
448
|
+
|
|
449
|
+
#### Filters Button Visibility
|
|
450
|
+
|
|
451
|
+
Show or hide the filters button:
|
|
452
|
+
|
|
453
|
+
```ruby
|
|
454
|
+
show_filters_button false # Hide filters button
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
---
|
|
458
|
+
|
|
459
|
+
## Formatters
|
|
460
|
+
|
|
461
|
+
Formatters customize how values are displayed.
|
|
462
|
+
|
|
463
|
+
### Built-in Formatters
|
|
464
|
+
|
|
465
|
+
#### 1. Date Formatter (`:date`)
|
|
466
|
+
|
|
467
|
+
```ruby
|
|
468
|
+
column :created_at,
|
|
469
|
+
type: :datetime,
|
|
470
|
+
formatter: :date,
|
|
471
|
+
formatter_options: { format: :short } # :short, :long, :datetime
|
|
472
|
+
```
|
|
473
|
+
|
|
474
|
+
#### 2. Currency Formatter (`:currency`)
|
|
475
|
+
|
|
476
|
+
```ruby
|
|
477
|
+
column :price,
|
|
478
|
+
type: :decimal,
|
|
479
|
+
formatter: :currency,
|
|
480
|
+
formatter_options: { precision: 2 }
|
|
481
|
+
```
|
|
482
|
+
|
|
483
|
+
#### 3. Boolean Formatter (`:boolean`)
|
|
484
|
+
|
|
485
|
+
```ruby
|
|
486
|
+
column :active,
|
|
487
|
+
type: :boolean,
|
|
488
|
+
formatter: :boolean
|
|
489
|
+
```
|
|
490
|
+
|
|
491
|
+
Displays checkmark (✓) for true, X (✗) for false.
|
|
492
|
+
|
|
493
|
+
#### 4. Badge Formatter (`:badge`)
|
|
494
|
+
|
|
495
|
+
```ruby
|
|
496
|
+
column :status,
|
|
497
|
+
type: :badge,
|
|
498
|
+
formatter: :badge,
|
|
499
|
+
formatter_options: {
|
|
500
|
+
colors: {
|
|
501
|
+
"active" => "green",
|
|
502
|
+
"inactive" => "gray",
|
|
503
|
+
"pending" => "yellow"
|
|
504
|
+
}
|
|
505
|
+
}
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
Available colors: `green`, `red`, `yellow`, `blue`, `indigo`, `purple`, `pink`, `gray`, `cyan`
|
|
509
|
+
|
|
510
|
+
#### 5. Action Formatter (`:action`)
|
|
511
|
+
|
|
512
|
+
```ruby
|
|
513
|
+
column :actions,
|
|
514
|
+
type: :action,
|
|
515
|
+
formatter: :action,
|
|
516
|
+
formatter_options: {
|
|
517
|
+
actions: [
|
|
518
|
+
{
|
|
519
|
+
label: "View",
|
|
520
|
+
url: ->(record) { "/users/#{record.id}" },
|
|
521
|
+
color: "primary" # primary, secondary, danger
|
|
522
|
+
},
|
|
523
|
+
{
|
|
524
|
+
label: "Delete",
|
|
525
|
+
url: ->(record) { "/users/#{record.id}" },
|
|
526
|
+
method: :delete,
|
|
527
|
+
color: "danger",
|
|
528
|
+
confirm: "Are you sure?"
|
|
529
|
+
}
|
|
530
|
+
],
|
|
531
|
+
style: :buttons # :buttons (default), :links or :dropdown (a keyboard-friendly menu, no JavaScript)
|
|
532
|
+
}
|
|
533
|
+
```
|
|
534
|
+
|
|
535
|
+
`method:` and `confirm:` use Turbo (`data-turbo-method`, `data-turbo-confirm`). Labels are HTML-escaped.
|
|
536
|
+
|
|
537
|
+
### Custom Formatters
|
|
538
|
+
|
|
539
|
+
#### Lambda/Proc Formatter
|
|
540
|
+
|
|
541
|
+
```ruby
|
|
542
|
+
column :name,
|
|
543
|
+
formatter: ->(value, record) { content_tag(:strong, value) }
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
The lambda receives:
|
|
547
|
+
- `value` - Column value
|
|
548
|
+
- `record` - Full record object
|
|
549
|
+
|
|
550
|
+
The returned string is rendered as-is when you mark it `html_safe`, so escape anything that comes from user data: `safe_join(["<strong>".html_safe, value, "</strong>".html_safe])` or `content_tag(:strong, value)`.
|
|
551
|
+
|
|
552
|
+
#### Hash Formatter
|
|
553
|
+
|
|
554
|
+
```ruby
|
|
555
|
+
column :status,
|
|
556
|
+
formatter: {
|
|
557
|
+
type: :badge,
|
|
558
|
+
options: { colors: { "active" => "green" } }
|
|
559
|
+
}
|
|
560
|
+
```
|
|
561
|
+
|
|
562
|
+
---
|
|
563
|
+
|
|
564
|
+
## Filters
|
|
565
|
+
|
|
566
|
+
### Filter Types
|
|
567
|
+
|
|
568
|
+
Filters are automatically enabled for columns with `filterable: true`.
|
|
569
|
+
|
|
570
|
+
#### Text Filter (`:text`)
|
|
571
|
+
|
|
572
|
+
```ruby
|
|
573
|
+
column :name,
|
|
574
|
+
filterable: true,
|
|
575
|
+
filter_type: :text
|
|
576
|
+
```
|
|
577
|
+
|
|
578
|
+
Operators: Contains, Starts with, Ends with, Equals, Does not contain
|
|
579
|
+
|
|
580
|
+
#### Number Filter (`:number`)
|
|
581
|
+
|
|
582
|
+
```ruby
|
|
583
|
+
column :age,
|
|
584
|
+
filterable: true,
|
|
585
|
+
filter_type: :number
|
|
586
|
+
```
|
|
587
|
+
|
|
588
|
+
Operators: Equals, Greater than, Less than, Greater or equal, Less or equal, Between
|
|
589
|
+
|
|
590
|
+
#### Date Filter (`:date`)
|
|
591
|
+
|
|
592
|
+
```ruby
|
|
593
|
+
column :created_at,
|
|
594
|
+
filterable: true,
|
|
595
|
+
filter_type: :date
|
|
596
|
+
```
|
|
597
|
+
|
|
598
|
+
Operators: On, Before, After, Between
|
|
599
|
+
|
|
600
|
+
#### Select Filter (`:select`)
|
|
601
|
+
|
|
602
|
+
```ruby
|
|
603
|
+
column :status,
|
|
604
|
+
filterable: true,
|
|
605
|
+
filter_type: :select,
|
|
606
|
+
filter_options: ["active", "inactive", "pending"]
|
|
607
|
+
|
|
608
|
+
# Or with dynamic options (Proc)
|
|
609
|
+
column :company,
|
|
610
|
+
filterable: true,
|
|
611
|
+
filter_type: :select,
|
|
612
|
+
filter_options: -> { Company.pluck(:name) }
|
|
613
|
+
|
|
614
|
+
# Or with custom label/value pairs
|
|
615
|
+
column :status,
|
|
616
|
+
filterable: true,
|
|
617
|
+
filter_type: :select,
|
|
618
|
+
filter_options: [["Active", "active"], ["Inactive", "inactive"]]
|
|
619
|
+
```
|
|
620
|
+
|
|
621
|
+
#### Boolean Filter (`:boolean`)
|
|
622
|
+
|
|
623
|
+
```ruby
|
|
624
|
+
column :active,
|
|
625
|
+
filterable: true,
|
|
626
|
+
filter_type: :boolean
|
|
627
|
+
```
|
|
628
|
+
|
|
629
|
+
Options: True, False, Any
|
|
630
|
+
|
|
631
|
+
### Auto-apply Filters
|
|
632
|
+
|
|
633
|
+
Control filter application behavior:
|
|
634
|
+
|
|
635
|
+
```ruby
|
|
636
|
+
# Global (in initializer)
|
|
637
|
+
config.autofilter = true # Apply filters automatically (debounced)
|
|
638
|
+
config.autofilter = false # Require "Apply Filters" button click
|
|
639
|
+
|
|
640
|
+
# Per-table (in view)
|
|
641
|
+
<%= infotable :users_table, autofilter: false %>
|
|
642
|
+
```
|
|
643
|
+
|
|
644
|
+
---
|
|
645
|
+
|
|
646
|
+
## View Helpers
|
|
647
|
+
|
|
648
|
+
### Basic Usage
|
|
649
|
+
|
|
650
|
+
```ruby
|
|
651
|
+
<%= infotable :users_table %>
|
|
652
|
+
```
|
|
653
|
+
|
|
654
|
+
### Available Options
|
|
655
|
+
|
|
656
|
+
```ruby
|
|
657
|
+
<%= infotable :users_table,
|
|
658
|
+
title: "Active Users",
|
|
659
|
+
subtitle: "Currently logged in users",
|
|
660
|
+
autofilter: false,
|
|
661
|
+
per_page: 50,
|
|
662
|
+
per_page_options: [25, 50, 100],
|
|
663
|
+
drawer_position: :left,
|
|
664
|
+
drawer_width: "500px",
|
|
665
|
+
theme: :default
|
|
666
|
+
%>
|
|
667
|
+
```
|
|
668
|
+
|
|
669
|
+
| Option | Type | Description |
|
|
670
|
+
|--------|------|-------------|
|
|
671
|
+
| `title` | String | Table title (defaults to table class name) |
|
|
672
|
+
| `subtitle` | String | Optional subtitle (not shown if nil) |
|
|
673
|
+
| `autofilter` | Boolean | Override global autofilter setting |
|
|
674
|
+
| `per_page` | Integer | Override default per page |
|
|
675
|
+
| `per_page_options` | Array | Override per page options |
|
|
676
|
+
| `drawer_position` | Symbol | Override drawer position |
|
|
677
|
+
| `drawer_width` | String | Override drawer width |
|
|
678
|
+
| `theme` | Symbol | Override theme |
|
|
679
|
+
|
|
680
|
+
---
|
|
681
|
+
|
|
682
|
+
## Internationalization (i18n)
|
|
683
|
+
|
|
684
|
+
InfoTable includes English and Polish translations.
|
|
685
|
+
|
|
686
|
+
### Set Locale
|
|
687
|
+
|
|
688
|
+
```ruby
|
|
689
|
+
# config/application.rb
|
|
690
|
+
config.i18n.default_locale = :en
|
|
691
|
+
config.i18n.available_locales = [:en, :pl]
|
|
692
|
+
|
|
693
|
+
# Or per-request
|
|
694
|
+
class ApplicationController < ActionController::Base
|
|
695
|
+
before_action :set_locale
|
|
696
|
+
|
|
697
|
+
def set_locale
|
|
698
|
+
I18n.locale = params[:locale] || I18n.default_locale
|
|
699
|
+
end
|
|
700
|
+
end
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
### Available Languages
|
|
704
|
+
|
|
705
|
+
- English (`:en`) - Default
|
|
706
|
+
- Polish (`:pl`)
|
|
707
|
+
|
|
708
|
+
### Customize Translations
|
|
709
|
+
|
|
710
|
+
Override translations in your app:
|
|
711
|
+
|
|
712
|
+
```yaml
|
|
713
|
+
# config/locales/infotable.en.yml
|
|
714
|
+
en:
|
|
715
|
+
infotable:
|
|
716
|
+
table:
|
|
717
|
+
buttons:
|
|
718
|
+
export_csv: "Download CSV"
|
|
719
|
+
```
|
|
720
|
+
|
|
721
|
+
### Add New Language
|
|
722
|
+
|
|
723
|
+
```yaml
|
|
724
|
+
# config/locales/infotable.es.yml
|
|
725
|
+
es:
|
|
726
|
+
infotable:
|
|
727
|
+
table:
|
|
728
|
+
buttons:
|
|
729
|
+
export_csv: "Exportar CSV"
|
|
730
|
+
filters: "Filtros"
|
|
731
|
+
# ... see config/locales/en.yml for full structure
|
|
732
|
+
```
|
|
733
|
+
|
|
734
|
+
See [docs/i18n.md](docs/i18n.md) for complete translation documentation.
|
|
735
|
+
|
|
736
|
+
---
|
|
737
|
+
|
|
738
|
+
## Advanced Features
|
|
739
|
+
|
|
740
|
+
### Column Visibility & Ordering
|
|
741
|
+
|
|
742
|
+
Users can:
|
|
743
|
+
- Show/hide columns
|
|
744
|
+
- Drag to reorder columns
|
|
745
|
+
- Pin columns to left or right
|
|
746
|
+
- Reset to defaults
|
|
747
|
+
|
|
748
|
+
Changes persist in localStorage per table.
|
|
749
|
+
|
|
750
|
+
### Pinned Columns
|
|
751
|
+
|
|
752
|
+
Pin columns by default:
|
|
753
|
+
|
|
754
|
+
```ruby
|
|
755
|
+
column :id, pinned: :left # Pin to left
|
|
756
|
+
column :actions, pinned: :right # Pin to right
|
|
757
|
+
```
|
|
758
|
+
|
|
759
|
+
Users can also pin/unpin columns via the UI.
|
|
760
|
+
|
|
761
|
+
### Sticky Header
|
|
762
|
+
|
|
763
|
+
Table headers remain visible while scrolling through table content.
|
|
764
|
+
|
|
765
|
+
### Export to CSV
|
|
766
|
+
|
|
767
|
+
Built-in CSV export with:
|
|
768
|
+
- Current filters applied
|
|
769
|
+
- Current sort order
|
|
770
|
+
- Only visible columns
|
|
771
|
+
- All pages (not just current)
|
|
772
|
+
|
|
773
|
+
---
|
|
774
|
+
|
|
775
|
+
## Examples
|
|
776
|
+
|
|
777
|
+
### Basic Table
|
|
778
|
+
|
|
779
|
+
```ruby
|
|
780
|
+
class UsersTable < Infotable::Base
|
|
781
|
+
query { User.all }
|
|
782
|
+
|
|
783
|
+
column :id, sortable: true
|
|
784
|
+
column :name, sortable: true, searchable: true
|
|
785
|
+
column :email, sortable: true
|
|
786
|
+
column :created_at, type: :datetime, sortable: true
|
|
787
|
+
|
|
788
|
+
per_page 25
|
|
789
|
+
default_sort :created_at, :desc
|
|
790
|
+
end
|
|
791
|
+
```
|
|
792
|
+
|
|
793
|
+
### Table with Filters
|
|
794
|
+
|
|
795
|
+
```ruby
|
|
796
|
+
class ProductsTable < Infotable::Base
|
|
797
|
+
query { Product.all }
|
|
798
|
+
|
|
799
|
+
column :name,
|
|
800
|
+
sortable: true,
|
|
801
|
+
filterable: true,
|
|
802
|
+
filter_type: :text
|
|
803
|
+
|
|
804
|
+
column :category,
|
|
805
|
+
sortable: true,
|
|
806
|
+
filterable: true,
|
|
807
|
+
filter_type: :select,
|
|
808
|
+
filter_options: -> { Product.distinct.pluck(:category) }
|
|
809
|
+
|
|
810
|
+
column :price,
|
|
811
|
+
type: :decimal,
|
|
812
|
+
sortable: true,
|
|
813
|
+
filterable: true,
|
|
814
|
+
filter_type: :number,
|
|
815
|
+
formatter: :currency
|
|
816
|
+
|
|
817
|
+
column :in_stock,
|
|
818
|
+
type: :boolean,
|
|
819
|
+
filterable: true,
|
|
820
|
+
formatter: :boolean
|
|
821
|
+
|
|
822
|
+
per_page 25
|
|
823
|
+
end
|
|
824
|
+
```
|
|
825
|
+
|
|
826
|
+
### Table with Custom Formatters
|
|
827
|
+
|
|
828
|
+
```ruby
|
|
829
|
+
class OrdersTable < Infotable::Base
|
|
830
|
+
query { Order.includes(:customer) }
|
|
831
|
+
|
|
832
|
+
column :order_number,
|
|
833
|
+
formatter: ->(value, record) {
|
|
834
|
+
"<code class='font-mono'>#{value}</code>".html_safe
|
|
835
|
+
}
|
|
836
|
+
|
|
837
|
+
column :customer,
|
|
838
|
+
formatter: ->(value, record) { record.customer.name }
|
|
839
|
+
|
|
840
|
+
column :status,
|
|
841
|
+
type: :badge,
|
|
842
|
+
formatter: :badge,
|
|
843
|
+
formatter_options: {
|
|
844
|
+
colors: {
|
|
845
|
+
"pending" => "yellow",
|
|
846
|
+
"processing" => "blue",
|
|
847
|
+
"shipped" => "green",
|
|
848
|
+
"delivered" => "green",
|
|
849
|
+
"cancelled" => "red"
|
|
850
|
+
}
|
|
851
|
+
}
|
|
852
|
+
|
|
853
|
+
column :total,
|
|
854
|
+
type: :currency,
|
|
855
|
+
formatter: :currency,
|
|
856
|
+
formatter_options: { precision: 2 }
|
|
857
|
+
|
|
858
|
+
column :actions,
|
|
859
|
+
type: :action,
|
|
860
|
+
formatter: :action,
|
|
861
|
+
formatter_options: {
|
|
862
|
+
actions: [
|
|
863
|
+
{ label: "View", url: ->(r) { "/orders/#{r.id}" } },
|
|
864
|
+
{ label: "Invoice", url: ->(r) { "/orders/#{r.id}/invoice" } }
|
|
865
|
+
]
|
|
866
|
+
}
|
|
867
|
+
end
|
|
868
|
+
```
|
|
869
|
+
|
|
870
|
+
### Infinite Scroll Table
|
|
871
|
+
|
|
872
|
+
```ruby
|
|
873
|
+
class PostsTable < Infotable::Base
|
|
874
|
+
query { Post.published.includes(:author) }
|
|
875
|
+
|
|
876
|
+
column :title, sortable: true
|
|
877
|
+
column :author, sortable: true
|
|
878
|
+
column :published_at, type: :datetime, sortable: true
|
|
879
|
+
|
|
880
|
+
infinite_scroll true
|
|
881
|
+
per_page 50
|
|
882
|
+
default_sort :published_at, :desc
|
|
883
|
+
end
|
|
884
|
+
```
|
|
885
|
+
|
|
886
|
+
### Clickable Rows Table
|
|
887
|
+
|
|
888
|
+
```ruby
|
|
889
|
+
class CommentsTable < Infotable::Base
|
|
890
|
+
query { Comment.includes(:post) }
|
|
891
|
+
|
|
892
|
+
column :body, type: :text
|
|
893
|
+
column :author_name
|
|
894
|
+
column :created_at, type: :datetime
|
|
895
|
+
|
|
896
|
+
# Make rows clickable
|
|
897
|
+
row_link ->(record) { "/comments/#{record.id}" }
|
|
898
|
+
|
|
899
|
+
# Use slim rows for compact display
|
|
900
|
+
row_size :slim
|
|
901
|
+
end
|
|
902
|
+
```
|
|
903
|
+
|
|
904
|
+
### Table with Pinned Columns
|
|
905
|
+
|
|
906
|
+
```ruby
|
|
907
|
+
class SalesTable < Infotable::Base
|
|
908
|
+
query { Sale.all }
|
|
909
|
+
|
|
910
|
+
column :id, pinned: :left, width: "60px"
|
|
911
|
+
column :product_name
|
|
912
|
+
column :quantity
|
|
913
|
+
column :price, type: :currency
|
|
914
|
+
column :total, type: :currency
|
|
915
|
+
column :actions, pinned: :right, type: :action
|
|
916
|
+
end
|
|
917
|
+
```
|
|
918
|
+
|
|
919
|
+
### Tables with Different Row Sizes
|
|
920
|
+
|
|
921
|
+
```ruby
|
|
922
|
+
# Slim - Compact for dense data
|
|
923
|
+
class CompactDataTable < Infotable::Base
|
|
924
|
+
query { Data.all }
|
|
925
|
+
|
|
926
|
+
column :id
|
|
927
|
+
column :name
|
|
928
|
+
column :value
|
|
929
|
+
|
|
930
|
+
row_size :slim # Minimal padding, smaller text
|
|
931
|
+
end
|
|
932
|
+
|
|
933
|
+
# Mid - Balanced appearance
|
|
934
|
+
class BalancedTable < Infotable::Base
|
|
935
|
+
query { Product.all }
|
|
936
|
+
|
|
937
|
+
column :id
|
|
938
|
+
column :name
|
|
939
|
+
column :price, type: :currency
|
|
940
|
+
|
|
941
|
+
row_size :mid # Medium padding, normal text
|
|
942
|
+
end
|
|
943
|
+
|
|
944
|
+
# Default - Comfortable reading
|
|
945
|
+
class ReadableTable < Infotable::Base
|
|
946
|
+
query { Article.all }
|
|
947
|
+
|
|
948
|
+
column :title
|
|
949
|
+
column :author
|
|
950
|
+
column :published_at, type: :datetime
|
|
951
|
+
|
|
952
|
+
row_size :default # Full padding (or omit - it's the default)
|
|
953
|
+
end
|
|
954
|
+
```
|
|
955
|
+
|
|
956
|
+
### Table with Virtual (Aggregated) Columns
|
|
957
|
+
|
|
958
|
+
```ruby
|
|
959
|
+
class ShopsTable < Infotable::Base
|
|
960
|
+
# Query with aggregated columns
|
|
961
|
+
query do
|
|
962
|
+
Shop.select(
|
|
963
|
+
"shops.*",
|
|
964
|
+
"COUNT(products.id) AS products_count",
|
|
965
|
+
"COUNT(accessories.id) AS accessories_count",
|
|
966
|
+
"SUM(products.price) AS total_products_value"
|
|
967
|
+
)
|
|
968
|
+
.left_joins(:products, :accessories)
|
|
969
|
+
.group("shops.id")
|
|
970
|
+
end
|
|
971
|
+
|
|
972
|
+
# Regular columns - use WHERE for filtering
|
|
973
|
+
column :id, sortable: true
|
|
974
|
+
column :name, sortable: true, filterable: true, filter_type: :text
|
|
975
|
+
column :active, type: :boolean, sortable: true, filterable: true
|
|
976
|
+
|
|
977
|
+
# Virtual columns - use HAVING for filtering
|
|
978
|
+
column :products_count,
|
|
979
|
+
label: "Products",
|
|
980
|
+
type: :integer,
|
|
981
|
+
sortable: true,
|
|
982
|
+
filterable: true,
|
|
983
|
+
filter_type: :number,
|
|
984
|
+
virtual: true, # ⭐ Enables HAVING clause for filters
|
|
985
|
+
virtual_sql: "COUNT(products.id)" # SQL expression for filtering
|
|
986
|
+
|
|
987
|
+
column :accessories_count,
|
|
988
|
+
label: "Accessories",
|
|
989
|
+
type: :integer,
|
|
990
|
+
sortable: true,
|
|
991
|
+
filterable: true,
|
|
992
|
+
filter_type: :number,
|
|
993
|
+
virtual: true, # ⭐ Enables HAVING clause for filters
|
|
994
|
+
virtual_sql: "COUNT(accessories.id)" # SQL expression for filtering
|
|
995
|
+
|
|
996
|
+
column :total_products_value,
|
|
997
|
+
label: "Total Value",
|
|
998
|
+
type: :decimal,
|
|
999
|
+
sortable: true,
|
|
1000
|
+
filterable: true,
|
|
1001
|
+
filter_type: :number,
|
|
1002
|
+
virtual: true, # ⭐ Enables HAVING clause for filters
|
|
1003
|
+
virtual_sql: "SUM(products.price)", # SQL expression for filtering
|
|
1004
|
+
formatter: :currency
|
|
1005
|
+
|
|
1006
|
+
per_page 25
|
|
1007
|
+
end
|
|
1008
|
+
```
|
|
1009
|
+
|
|
1010
|
+
**See [docs/virtual_columns.md](docs/virtual_columns.md) for more examples and detailed explanation.**
|
|
1011
|
+
|
|
1012
|
+
---
|
|
1013
|
+
|
|
1014
|
+
## Roadmap
|
|
1015
|
+
|
|
1016
|
+
- [ ] Server-side persistence of column settings
|
|
1017
|
+
- [ ] Table state in the URL
|
|
1018
|
+
- [ ] More formatters (link, avatar, percent)
|
|
1019
|
+
- [ ] `infotable:table` generator
|
|
1020
|
+
- [ ] Tailwind v3 support
|
|
1021
|
+
|
|
1022
|
+
---
|
|
1023
|
+
|
|
1024
|
+
## Documentation
|
|
1025
|
+
|
|
1026
|
+
- [Installation Guide](docs/installation.md) - Detailed setup for all bundlers
|
|
1027
|
+
- [Tailwind setup](docs/tailwind.md)
|
|
1028
|
+
- [Bundler support](docs/bundlers.md)
|
|
1029
|
+
- [Virtual columns](docs/virtual_columns.md)
|
|
1030
|
+
- [i18n Guide](docs/i18n.md)
|
|
1031
|
+
- [Changelog](CHANGELOG.md)
|
|
1032
|
+
|
|
1033
|
+
---
|
|
1034
|
+
|
|
1035
|
+
## Contributing
|
|
1036
|
+
|
|
1037
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jcieslar/infotable.
|
|
1038
|
+
|
|
1039
|
+
---
|
|
1040
|
+
|
|
1041
|
+
## License
|
|
1042
|
+
|
|
1043
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|