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
|
@@ -0,0 +1,463 @@
|
|
|
1
|
+
module Infotable
|
|
2
|
+
class Base
|
|
3
|
+
class << self
|
|
4
|
+
attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :slim_rows_settings, :row_size_settings, :filters_button_settings, :display_settings, :nil_value_settings, :empty_value_settings, :table_authorize, :checkbox_settings, :batch_action_list, :countless_setting, :url_state_setting
|
|
5
|
+
|
|
6
|
+
def inherited(subclass)
|
|
7
|
+
super
|
|
8
|
+
subclass.table_columns = []
|
|
9
|
+
subclass.pagination_settings = {
|
|
10
|
+
per_page: Infotable.configuration.default_per_page,
|
|
11
|
+
per_page_options: Infotable.configuration.per_page_options
|
|
12
|
+
}
|
|
13
|
+
subclass.sort_settings = {}
|
|
14
|
+
subclass.height_settings = {
|
|
15
|
+
min_height: nil, # nil means use global config
|
|
16
|
+
max_height: nil # nil means use global config
|
|
17
|
+
}
|
|
18
|
+
subclass.infinite_scroll_settings = {
|
|
19
|
+
enabled: false
|
|
20
|
+
}
|
|
21
|
+
subclass.row_link_settings = {
|
|
22
|
+
enabled: false,
|
|
23
|
+
url: nil
|
|
24
|
+
}
|
|
25
|
+
subclass.slim_rows_settings = {
|
|
26
|
+
enabled: false
|
|
27
|
+
}
|
|
28
|
+
subclass.row_size_settings = {
|
|
29
|
+
size: :default # :slim, :mid, :default
|
|
30
|
+
}
|
|
31
|
+
subclass.filters_button_settings = {
|
|
32
|
+
visible: true
|
|
33
|
+
}
|
|
34
|
+
subclass.display_settings = {
|
|
35
|
+
title: nil,
|
|
36
|
+
subtitle: nil
|
|
37
|
+
}
|
|
38
|
+
subclass.nil_value_settings = {
|
|
39
|
+
value: nil # nil means use global config
|
|
40
|
+
}
|
|
41
|
+
subclass.empty_value_settings = {
|
|
42
|
+
value: nil # nil means use global config
|
|
43
|
+
}
|
|
44
|
+
subclass.checkbox_settings = { enabled: false, id_method: :id }
|
|
45
|
+
subclass.batch_action_list = []
|
|
46
|
+
subclass.countless_setting = false
|
|
47
|
+
subclass.url_state_setting = nil
|
|
48
|
+
|
|
49
|
+
inherit_from_parent(subclass)
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
# Copy parent table definition so a subclass can extend it
|
|
53
|
+
def inherit_from_parent(subclass)
|
|
54
|
+
parent = subclass.superclass
|
|
55
|
+
return if parent == Infotable::Base || parent.table_columns.nil?
|
|
56
|
+
|
|
57
|
+
subclass.table_columns = parent.table_columns.map(&:dup)
|
|
58
|
+
subclass.table_query = parent.table_query
|
|
59
|
+
subclass.table_query_sql = parent.table_query_sql
|
|
60
|
+
subclass.table_authorize = parent.table_authorize
|
|
61
|
+
subclass.checkbox_settings = parent.checkbox_settings.dup
|
|
62
|
+
subclass.batch_action_list = parent.batch_action_list.map(&:dup)
|
|
63
|
+
subclass.countless_setting = parent.countless_setting
|
|
64
|
+
subclass.url_state_setting = parent.url_state_setting
|
|
65
|
+
%i[pagination_settings sort_settings height_settings infinite_scroll_settings row_link_settings
|
|
66
|
+
slim_rows_settings row_size_settings filters_button_settings display_settings
|
|
67
|
+
nil_value_settings empty_value_settings].each do |attr|
|
|
68
|
+
subclass.public_send("#{attr}=", parent.public_send(attr).dup)
|
|
69
|
+
end
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
# Define data source using ActiveRecord.
|
|
73
|
+
# The block receives the controller (current_user, params, ...) for per-request scoping:
|
|
74
|
+
# query { |controller| Post.where(account: controller.current_account) }
|
|
75
|
+
def query(&block)
|
|
76
|
+
@table_query = block
|
|
77
|
+
end
|
|
78
|
+
|
|
79
|
+
# Skip SELECT COUNT(*) (slow on big tables): no total, no page numbers, only prev/next.
|
|
80
|
+
def countless(enabled = true)
|
|
81
|
+
@countless_setting = enabled
|
|
82
|
+
end
|
|
83
|
+
|
|
84
|
+
# Keep search, filters, sort and page in the page's address bar (shareable links, working Back
|
|
85
|
+
# button). Default: Infotable.configuration.url_state. Turn it off for tables in modals/tabs
|
|
86
|
+
# or when several unrelated tables should not fight over the URL.
|
|
87
|
+
def url_state(enabled = true)
|
|
88
|
+
@url_state_setting = enabled
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def url_state?
|
|
92
|
+
@url_state_setting.nil? ? Infotable.configuration.url_state : @url_state_setting ? true : false
|
|
93
|
+
end
|
|
94
|
+
|
|
95
|
+
def countless?
|
|
96
|
+
@countless_setting ? true : false
|
|
97
|
+
end
|
|
98
|
+
|
|
99
|
+
# Row checkboxes without bulk actions (e.g. to read the selection from your own JS)
|
|
100
|
+
def checkbox_column(enabled: true, id_method: :id)
|
|
101
|
+
@checkbox_settings = { enabled: enabled, id_method: id_method }
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
# Bulk actions; defining any turns the checkbox column on. See Infotable::BatchActions.
|
|
105
|
+
def batch_actions(&block)
|
|
106
|
+
builder = Infotable::BatchActions::Builder.new
|
|
107
|
+
builder.instance_eval(&block)
|
|
108
|
+
@batch_action_list = builder.actions
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
def checkbox_enabled?
|
|
112
|
+
@checkbox_settings[:enabled] || @batch_action_list.any?
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
def checkbox_value(record)
|
|
116
|
+
record.public_send(@checkbox_settings[:id_method])
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
# Per-table access check. Block receives the controller; falsy => 403.
|
|
120
|
+
# authorize { |controller| controller.current_user&.admin? }
|
|
121
|
+
def authorize(&block)
|
|
122
|
+
@table_authorize = block
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def authorized?(context = nil)
|
|
126
|
+
@table_authorize.nil? || @table_authorize.call(context) ? true : false
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
# Define data source using raw SQL
|
|
130
|
+
# Raw SQL data source. The SQL becomes a sub-select aliased as the model's table, so
|
|
131
|
+
# filters, search, sorting, pagination and export work like with `query`:
|
|
132
|
+
# query_sql "SELECT u.*, COUNT(p.id) AS posts_n FROM users u LEFT JOIN posts p ON ... GROUP BY u.id", model: User
|
|
133
|
+
# The SQL is developer-defined (never interpolate request input into it).
|
|
134
|
+
def query_sql(sql, model:)
|
|
135
|
+
raise ArgumentError, "query_sql requires a model: (an ActiveRecord class used for types and the table alias)" unless model.respond_to?(:table_name)
|
|
136
|
+
|
|
137
|
+
@table_query_sql = { sql: sql.to_s, model: model }
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
# Define a column
|
|
141
|
+
def column(name, **options)
|
|
142
|
+
@table_columns ||= []
|
|
143
|
+
type = options[:type] || :string
|
|
144
|
+
|
|
145
|
+
# Determine width - use custom if provided and not blank, otherwise use default
|
|
146
|
+
width = if options[:width].present?
|
|
147
|
+
options[:width]
|
|
148
|
+
else
|
|
149
|
+
default_column_width(type)
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
column_definition = {
|
|
153
|
+
name: name,
|
|
154
|
+
label: options[:label] || name.to_s.tr("_", " "),
|
|
155
|
+
type: type,
|
|
156
|
+
sortable: options.fetch(:sortable, false),
|
|
157
|
+
searchable: options.fetch(:searchable, false),
|
|
158
|
+
filterable: options.fetch(:filterable, false),
|
|
159
|
+
visible: options.fetch(:visible, true),
|
|
160
|
+
pinned: options[:pinned], # :left, :right, or nil
|
|
161
|
+
virtual: options.fetch(:virtual, false), # Virtual/computed columns (aggregates, subqueries)
|
|
162
|
+
virtual_sql: options[:virtual_sql], # SQL expression for aggregate columns (used in HAVING clause)
|
|
163
|
+
sql: options[:sql], # Row-level SQL expression for filtering/sorting (WHERE), e.g. "authors.name"
|
|
164
|
+
sort_sql: options[:sort_sql], # Overrides :sql for ORDER BY only, e.g. filter by id, sort by name
|
|
165
|
+
value: options[:value], # ->(record) { ... } reads the cell value when it is not a record attribute
|
|
166
|
+
includes: options[:includes], # association(s) to preload for this column (avoids N+1), e.g. :author
|
|
167
|
+
formatter: options[:formatter],
|
|
168
|
+
formatter_options: options[:formatter_options],
|
|
169
|
+
actions: options[:actions], # For action columns (deprecated - use formatter_options)
|
|
170
|
+
filter_type: options[:filter_type],
|
|
171
|
+
filter_options: options[:filter_options],
|
|
172
|
+
filter_options_cache: options[:filter_options_cache], # e.g. 5.minutes: cache a Proc's result (not for per-user options)
|
|
173
|
+
filter_options_limit: options[:filter_options_limit], # max <option>s rendered; default Infotable.configuration.max_filter_options
|
|
174
|
+
filter_multi_select: options.fetch(:filter_multi_select, false),
|
|
175
|
+
width: width
|
|
176
|
+
}
|
|
177
|
+
@table_columns << column_definition
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
# Get default width based on column type
|
|
181
|
+
def default_column_width(type)
|
|
182
|
+
case type
|
|
183
|
+
when :boolean
|
|
184
|
+
"80px"
|
|
185
|
+
when :date
|
|
186
|
+
"120px"
|
|
187
|
+
when :datetime
|
|
188
|
+
"180px"
|
|
189
|
+
when :integer
|
|
190
|
+
"100px"
|
|
191
|
+
when :decimal, :float, :currency
|
|
192
|
+
"120px"
|
|
193
|
+
when :badge, :status
|
|
194
|
+
"120px"
|
|
195
|
+
when :action, :actions
|
|
196
|
+
"100px"
|
|
197
|
+
when :text
|
|
198
|
+
"300px"
|
|
199
|
+
when :string
|
|
200
|
+
"200px"
|
|
201
|
+
else
|
|
202
|
+
"200px"
|
|
203
|
+
end
|
|
204
|
+
end
|
|
205
|
+
|
|
206
|
+
# Set pagination per_page
|
|
207
|
+
def per_page(value)
|
|
208
|
+
@pagination_settings[:per_page] = value
|
|
209
|
+
end
|
|
210
|
+
|
|
211
|
+
# Set per_page options
|
|
212
|
+
def per_page_options(values)
|
|
213
|
+
@pagination_settings[:per_page_options] = values
|
|
214
|
+
end
|
|
215
|
+
|
|
216
|
+
# Set default sort
|
|
217
|
+
def default_sort(column, direction = :asc)
|
|
218
|
+
@sort_settings = { column: column, direction: direction }
|
|
219
|
+
end
|
|
220
|
+
|
|
221
|
+
# Set table minimum height
|
|
222
|
+
def table_min_height(value)
|
|
223
|
+
@height_settings[:min_height] = value
|
|
224
|
+
end
|
|
225
|
+
|
|
226
|
+
# Set table maximum height
|
|
227
|
+
def table_max_height(value)
|
|
228
|
+
@height_settings[:max_height] = value
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# Enable/disable infinite scroll
|
|
232
|
+
def infinite_scroll(enabled)
|
|
233
|
+
@infinite_scroll_settings[:enabled] = enabled
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
# Check if infinite scroll is enabled
|
|
237
|
+
def infinite_scroll_enabled?
|
|
238
|
+
@infinite_scroll_settings[:enabled]
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
# Configure row link
|
|
242
|
+
# @param url [Proc, String] - URL generator (Proc receives record) or static URL
|
|
243
|
+
# @example
|
|
244
|
+
# row_link ->(record) { "/comments/#{record.id}" }
|
|
245
|
+
# row_link "/posts/show"
|
|
246
|
+
def row_link(url)
|
|
247
|
+
@row_link_settings[:enabled] = true
|
|
248
|
+
@row_link_settings[:url] = url
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Check if row link is enabled
|
|
252
|
+
def row_link_enabled?
|
|
253
|
+
@row_link_settings[:enabled]
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
# Get row link URL for a record
|
|
257
|
+
def row_link_url(record)
|
|
258
|
+
return nil unless row_link_enabled?
|
|
259
|
+
|
|
260
|
+
url_generator = @row_link_settings[:url]
|
|
261
|
+
if url_generator.is_a?(Proc)
|
|
262
|
+
url_generator.call(record)
|
|
263
|
+
else
|
|
264
|
+
url_generator
|
|
265
|
+
end
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
# Set row size (padding and text size)
|
|
269
|
+
# @param size [Symbol] - Row size (:slim, :mid, :default)
|
|
270
|
+
# @example
|
|
271
|
+
# row_size :slim
|
|
272
|
+
# row_size :mid
|
|
273
|
+
# row_size :default
|
|
274
|
+
def row_size(size)
|
|
275
|
+
valid_sizes = [ :slim, :mid, :default ]
|
|
276
|
+
unless valid_sizes.include?(size)
|
|
277
|
+
raise ArgumentError, "Invalid row size: #{size}. Must be one of #{valid_sizes.join(', ')}"
|
|
278
|
+
end
|
|
279
|
+
@row_size_settings ||= { size: :default }
|
|
280
|
+
@row_size_settings[:size] = size
|
|
281
|
+
|
|
282
|
+
# Also set slim_rows for backward compatibility
|
|
283
|
+
@slim_rows_settings ||= { enabled: false }
|
|
284
|
+
@slim_rows_settings[:enabled] = (size == :slim)
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
# Get current row size
|
|
288
|
+
def row_size_value
|
|
289
|
+
@row_size_settings ||= { size: :default }
|
|
290
|
+
|
|
291
|
+
# Check slim_rows for backward compatibility
|
|
292
|
+
if @slim_rows_settings && @slim_rows_settings[:enabled]
|
|
293
|
+
return :slim
|
|
294
|
+
end
|
|
295
|
+
|
|
296
|
+
@row_size_settings[:size]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
# Enable slim rows (compact padding) - DEPRECATED: Use row_size(:slim) instead
|
|
300
|
+
# @example
|
|
301
|
+
# slim_rows true
|
|
302
|
+
def slim_rows(enabled = true)
|
|
303
|
+
@slim_rows_settings ||= { enabled: false }
|
|
304
|
+
@slim_rows_settings[:enabled] = enabled
|
|
305
|
+
|
|
306
|
+
# Also update row_size_settings for consistency
|
|
307
|
+
@row_size_settings ||= { size: :default }
|
|
308
|
+
@row_size_settings[:size] = enabled ? :slim : :default
|
|
309
|
+
end
|
|
310
|
+
|
|
311
|
+
# Check if slim rows is enabled - DEPRECATED: Use row_size_value instead
|
|
312
|
+
def slim_rows_enabled?
|
|
313
|
+
row_size_value == :slim
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
# Show/hide filters button
|
|
317
|
+
# @param visible [Boolean] - Whether to show the filters button
|
|
318
|
+
# @example
|
|
319
|
+
# show_filters_button false
|
|
320
|
+
def show_filters_button(visible = true)
|
|
321
|
+
@filters_button_settings ||= { visible: true }
|
|
322
|
+
@filters_button_settings[:visible] = visible
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
# Check if filters button is visible
|
|
326
|
+
def filters_button_visible?
|
|
327
|
+
@filters_button_settings ||= { visible: true }
|
|
328
|
+
@filters_button_settings[:visible]
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
# Set table title
|
|
332
|
+
# @param text [String] - The title to display above the table
|
|
333
|
+
# @example
|
|
334
|
+
# table_title "User Management"
|
|
335
|
+
def table_title(text)
|
|
336
|
+
@display_settings ||= { title: nil, subtitle: nil }
|
|
337
|
+
@display_settings[:title] = text
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
# Set table subtitle
|
|
341
|
+
# @param text [String] - The subtitle to display below the title
|
|
342
|
+
# @example
|
|
343
|
+
# table_subtitle "Manage all users in the system"
|
|
344
|
+
def table_subtitle(text)
|
|
345
|
+
@display_settings ||= { title: nil, subtitle: nil }
|
|
346
|
+
@display_settings[:subtitle] = text
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
# Get table title
|
|
350
|
+
def title
|
|
351
|
+
@display_settings ||= { title: nil, subtitle: nil }
|
|
352
|
+
@display_settings[:title]
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
# Get table subtitle
|
|
356
|
+
def subtitle
|
|
357
|
+
@display_settings ||= { title: nil, subtitle: nil }
|
|
358
|
+
@display_settings[:subtitle]
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Set nil value placeholder
|
|
362
|
+
# @param text [String] - The text to display for nil values
|
|
363
|
+
# @example
|
|
364
|
+
# nil_value "N/A"
|
|
365
|
+
# nil_value ""
|
|
366
|
+
def nil_value(text)
|
|
367
|
+
@nil_value_settings ||= { value: nil }
|
|
368
|
+
@nil_value_settings[:value] = text
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
# Get nil value placeholder (table-specific or global)
|
|
372
|
+
def nil_value_placeholder
|
|
373
|
+
@nil_value_settings ||= { value: nil }
|
|
374
|
+
@nil_value_settings[:value].nil? ? Infotable.configuration.nil_value : @nil_value_settings[:value]
|
|
375
|
+
end
|
|
376
|
+
|
|
377
|
+
# Set empty value placeholder
|
|
378
|
+
# @param text [String] - The text to display for empty strings
|
|
379
|
+
# @example
|
|
380
|
+
# empty_value "N/A"
|
|
381
|
+
# empty_value ""
|
|
382
|
+
def empty_value(text)
|
|
383
|
+
@empty_value_settings ||= { value: nil }
|
|
384
|
+
@empty_value_settings[:value] = text
|
|
385
|
+
end
|
|
386
|
+
|
|
387
|
+
# Get empty value placeholder (table-specific or global)
|
|
388
|
+
def empty_value_placeholder
|
|
389
|
+
@empty_value_settings ||= { value: nil }
|
|
390
|
+
@empty_value_settings[:value].nil? ? Infotable.configuration.empty_value : @empty_value_settings[:value]
|
|
391
|
+
end
|
|
392
|
+
|
|
393
|
+
# Get minimum height (table-specific or global)
|
|
394
|
+
def min_height
|
|
395
|
+
@height_settings[:min_height] || Infotable.configuration.table_min_height
|
|
396
|
+
end
|
|
397
|
+
|
|
398
|
+
# Get maximum height (table-specific or global)
|
|
399
|
+
def max_height
|
|
400
|
+
@height_settings[:max_height] || Infotable.configuration.table_max_height
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
# Get the base query
|
|
404
|
+
def base_query(context = nil)
|
|
405
|
+
if @table_query
|
|
406
|
+
@table_query.call(context)
|
|
407
|
+
elsif @table_query_sql
|
|
408
|
+
model = @table_query_sql[:model]
|
|
409
|
+
model.from(Arel.sql("(#{@table_query_sql[:sql]}) AS #{model.connection.quote_table_name(model.table_name)}"))
|
|
410
|
+
else
|
|
411
|
+
raise Infotable::Error, "No query or query_sql defined for #{name}"
|
|
412
|
+
end
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
# Raw cell value of a record for a column (value: proc, attribute or association)
|
|
416
|
+
def cell_value(record, column)
|
|
417
|
+
return column[:value].call(record) if column[:value]
|
|
418
|
+
|
|
419
|
+
record.try(column[:name]) || record[column[:name]]
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
# Associations to preload for the table's columns (deduplicated)
|
|
423
|
+
def preloads
|
|
424
|
+
columns.flat_map { |col| Array.wrap(col[:includes]) }.uniq
|
|
425
|
+
end
|
|
426
|
+
|
|
427
|
+
# Get all columns
|
|
428
|
+
def columns
|
|
429
|
+
@table_columns || []
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
# Get virtual (computed/aggregated) columns
|
|
433
|
+
def virtual_columns
|
|
434
|
+
columns.select { |col| col[:virtual] }
|
|
435
|
+
end
|
|
436
|
+
|
|
437
|
+
# Get non-virtual (regular database) columns
|
|
438
|
+
def regular_columns
|
|
439
|
+
columns.reject { |col| col[:virtual] }
|
|
440
|
+
end
|
|
441
|
+
|
|
442
|
+
# Get visible columns
|
|
443
|
+
def visible_columns
|
|
444
|
+
columns.select { |col| col[:visible] }
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
# Get sortable columns
|
|
448
|
+
def sortable_columns
|
|
449
|
+
columns.select { |col| col[:sortable] }
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
# Get filterable columns
|
|
453
|
+
def filterable_columns
|
|
454
|
+
columns.select { |col| col[:filterable] }
|
|
455
|
+
end
|
|
456
|
+
|
|
457
|
+
# Get searchable columns
|
|
458
|
+
def searchable_columns
|
|
459
|
+
columns.select { |col| col[:searchable] }
|
|
460
|
+
end
|
|
461
|
+
end
|
|
462
|
+
end
|
|
463
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
module Infotable
|
|
2
|
+
# Row selection + bulk actions. The gem renders the checkboxes and the action bar;
|
|
3
|
+
# the host app implements the endpoints and receives the ids as `ids[]`.
|
|
4
|
+
#
|
|
5
|
+
# class PostsTable < Infotable::Base
|
|
6
|
+
# batch_actions do
|
|
7
|
+
# action :archive, label: "Archive", url: "/posts/archive", method: :patch
|
|
8
|
+
# action :delete, label: "Delete", url: "/posts/delete", method: :delete,
|
|
9
|
+
# confirm: "Delete %{count} posts?", color: :danger
|
|
10
|
+
# end
|
|
11
|
+
# end
|
|
12
|
+
module BatchActions
|
|
13
|
+
METHODS = %i[get post put patch delete].freeze
|
|
14
|
+
COLORS = %i[default primary success warning danger].freeze
|
|
15
|
+
|
|
16
|
+
class Builder
|
|
17
|
+
attr_reader :actions
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@actions = []
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def action(name, url:, label: nil, method: :post, confirm: nil, color: :default)
|
|
24
|
+
method = method.to_s.downcase.to_sym
|
|
25
|
+
color = color.to_s.to_sym
|
|
26
|
+
raise ArgumentError, "batch action :#{name}: url must be a String" unless url.is_a?(String) && url.present?
|
|
27
|
+
raise ArgumentError, "batch action :#{name}: method must be one of #{METHODS.inspect}" unless METHODS.include?(method)
|
|
28
|
+
raise ArgumentError, "batch action :#{name}: color must be one of #{COLORS.inspect}" unless COLORS.include?(color)
|
|
29
|
+
|
|
30
|
+
@actions << { name: name.to_sym, label: label || name.to_s.humanize, url: url, method: method, confirm: confirm, color: color }
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
module Infotable
|
|
2
|
+
class Configuration
|
|
3
|
+
attr_accessor :default_per_page,
|
|
4
|
+
:per_page_options,
|
|
5
|
+
:drawer_position,
|
|
6
|
+
:drawer_width,
|
|
7
|
+
:theme,
|
|
8
|
+
:turbo_enabled,
|
|
9
|
+
:pagination_gem,
|
|
10
|
+
:table_min_height,
|
|
11
|
+
:table_max_height,
|
|
12
|
+
:autofilter,
|
|
13
|
+
:nil_value,
|
|
14
|
+
:empty_value,
|
|
15
|
+
:parent_controller,
|
|
16
|
+
:max_filter_options,
|
|
17
|
+
:url_state
|
|
18
|
+
|
|
19
|
+
def initialize
|
|
20
|
+
@default_per_page = 25
|
|
21
|
+
@per_page_options = [ 10, 25, 50, 100 ]
|
|
22
|
+
@drawer_position = :right # :left or :right
|
|
23
|
+
@drawer_width = "26rem"
|
|
24
|
+
@theme = :default # no effect yet (kept so existing initializers keep working)
|
|
25
|
+
@turbo_enabled = true # no effect yet
|
|
26
|
+
@pagination_gem = :pagy # no effect yet; Pagy is the only backend
|
|
27
|
+
@table_min_height = "400px"
|
|
28
|
+
@table_max_height = "600px"
|
|
29
|
+
@autofilter = true # true = auto-apply filters (debounced), false = manual apply button
|
|
30
|
+
@nil_value = "-" # Default placeholder for nil values
|
|
31
|
+
@empty_value = "-" # Default placeholder for empty strings
|
|
32
|
+
@url_state = true # mirror table state in the address bar
|
|
33
|
+
@max_filter_options = 500 # select filters render at most this many <option>s (nil = no limit)
|
|
34
|
+
@parent_controller = "ActionController::Base" # Set to "ApplicationController" to inherit host auth/filters
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
class << self
|
|
39
|
+
attr_writer :configuration
|
|
40
|
+
|
|
41
|
+
def configuration
|
|
42
|
+
@configuration ||= Configuration.new
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def configure
|
|
46
|
+
yield(configuration)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def reset_configuration!
|
|
50
|
+
@configuration = Configuration.new
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
require "turbo-rails"
|
|
2
|
+
require "stimulus-rails"
|
|
3
|
+
require "pagy"
|
|
4
|
+
|
|
5
|
+
module Infotable
|
|
6
|
+
class Engine < ::Rails::Engine
|
|
7
|
+
isolate_namespace Infotable
|
|
8
|
+
|
|
9
|
+
# Configure engine
|
|
10
|
+
config.infotable = ActiveSupport::OrderedOptions.new
|
|
11
|
+
|
|
12
|
+
# Default configuration values
|
|
13
|
+
config.infotable.table_min_height = "400px"
|
|
14
|
+
config.infotable.table_max_height = "600px"
|
|
15
|
+
|
|
16
|
+
# Rails already adds the engine's app/assets/* directories to the asset path;
|
|
17
|
+
# only the precompile list is needed. Skipped for apps without an asset pipeline.
|
|
18
|
+
initializer "infotable.assets" do |app|
|
|
19
|
+
if app.config.respond_to?(:assets)
|
|
20
|
+
app.config.assets.precompile += %w[infotable/controllers/*.js]
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
# Setup Stimulus controllers for importmap (only runs if importmap-rails is installed)
|
|
25
|
+
initializer "infotable.importmap", before: "importmap" do |app|
|
|
26
|
+
if app.config.respond_to?(:importmap)
|
|
27
|
+
app.config.importmap.paths << root.join("config/importmap.rb") if root.join("config/importmap.rb").exist?
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Add helpers to host application
|
|
32
|
+
initializer "infotable.helpers" do
|
|
33
|
+
ActiveSupport.on_load(:action_controller_base) do
|
|
34
|
+
helper Infotable::Engine.helpers
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# Load engine i18n locale files
|
|
39
|
+
initializer "infotable.i18n" do |app|
|
|
40
|
+
config.i18n.load_path += Dir[root.join("config/locales/**/*.yml")]
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
# Expose gem paths via ENV variables for Tailwind CSS v4 @source directives
|
|
44
|
+
# This allows Tailwind to scan the gem's views/formatters at build time
|
|
45
|
+
# Works in development, production, Docker, CI - everywhere!
|
|
46
|
+
initializer "infotable.tailwind", before: :load_config_initializers do
|
|
47
|
+
ENV["INFOTABLE_VIEWS_PATH"] = root.join("app/views/infotable").to_s
|
|
48
|
+
ENV["INFOTABLE_FORMATTERS_PATH"] = root.join("lib/infotable/formatters").to_s
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|