infotable 1.4.1 → 1.5.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 +4 -4
- data/README.md +35 -1
- data/app/assets/javascripts/infotable/controllers/drawer_controller.js +59 -364
- data/app/assets/javascripts/infotable/controllers/table_controller.js +71 -98
- data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
- data/app/controllers/infotable/tables_controller.rb +15 -4
- data/app/helpers/infotable/tables_helper.rb +0 -23
- data/app/views/infotable/tables/_column_visibility.html.erb +4 -4
- data/app/views/infotable/tables/_end_indicator.html.erb +8 -0
- data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
- data/app/views/infotable/tables/_table_rows.html.erb +9 -2
- data/app/views/infotable/tables/show.html.erb +2 -2
- data/config/importmap.rb +2 -2
- data/lib/generators/infotable/install/templates/initializer.rb +5 -3
- data/lib/infotable/base.rb +61 -4
- data/lib/infotable/configuration.rb +1 -1
- data/lib/infotable/engine.rb +1 -8
- data/lib/infotable/filter_processor.rb +10 -1
- data/lib/infotable/formatters/boolean_formatter.rb +4 -4
- data/lib/infotable/table_resolver.rb +9 -0
- data/lib/infotable/version.rb +1 -1
- metadata +3 -1
|
@@ -27,6 +27,10 @@ module Infotable
|
|
|
27
27
|
format.turbo_stream {
|
|
28
28
|
# For infinite scroll - return turbo stream to append rows and replace pagination frame
|
|
29
29
|
# Following the guide's "Frames + Streams" pattern
|
|
30
|
+
if @page_out_of_range # nothing left: appending the first page again would repeat its rows
|
|
31
|
+
return render turbo_stream: turbo_stream.replace("#{params[:table_name]}_pagination", partial: "infotable/tables/end_indicator")
|
|
32
|
+
end
|
|
33
|
+
|
|
30
34
|
streams = [
|
|
31
35
|
turbo_stream.append(
|
|
32
36
|
"#{params[:table_name]}_tbody",
|
|
@@ -48,7 +52,7 @@ module Infotable
|
|
|
48
52
|
# No more pages - replace with end indicator
|
|
49
53
|
streams << turbo_stream.replace(
|
|
50
54
|
"#{params[:table_name]}_pagination",
|
|
51
|
-
|
|
55
|
+
partial: "infotable/tables/end_indicator"
|
|
52
56
|
)
|
|
53
57
|
end
|
|
54
58
|
|
|
@@ -139,6 +143,7 @@ module Infotable
|
|
|
139
143
|
# Apply pagination
|
|
140
144
|
@pagy, @records = paginate(@page)
|
|
141
145
|
rescue Pagy::RangeError # page past the end: show the first page
|
|
146
|
+
@page_out_of_range = true
|
|
142
147
|
@pagy, @records = paginate(1)
|
|
143
148
|
end
|
|
144
149
|
|
|
@@ -207,7 +212,7 @@ module Infotable
|
|
|
207
212
|
def add_sort_tiebreaker(query, sort_columns)
|
|
208
213
|
model = query.model
|
|
209
214
|
pk = model.primary_key
|
|
210
|
-
return query if pk.blank? || sort_columns.map(&:to_s).include?(pk) || query.distinct_value
|
|
215
|
+
return query if pk.blank? || sort_columns.map(&:to_s).include?(pk) || query.distinct_value || query.group_values.any?
|
|
211
216
|
|
|
212
217
|
query.order(Arel.sql("#{model.table_name}.#{pk} ASC"))
|
|
213
218
|
end
|
|
@@ -228,9 +233,15 @@ module Infotable
|
|
|
228
233
|
requested.positive? ? [ requested, MAX_PER_PAGE ].min : default
|
|
229
234
|
end
|
|
230
235
|
|
|
231
|
-
|
|
236
|
+
PLAIN_NUMBER = /\A[+-]?\d+([.,]\d+)?\z/
|
|
237
|
+
|
|
238
|
+
# Guard against spreadsheet formula injection. Plain numbers ("-5", "+48123456789") are
|
|
239
|
+
# not formulas and are left alone.
|
|
232
240
|
def csv_safe(value)
|
|
233
|
-
value.is_a?(String) && value.match?(/\A[=+\-@\t\r]/)
|
|
241
|
+
return value unless value.is_a?(String) && value.match?(/\A[=+\-@\t\r]/)
|
|
242
|
+
return value if value.match?(PLAIN_NUMBER)
|
|
243
|
+
|
|
244
|
+
"'#{value}"
|
|
234
245
|
end
|
|
235
246
|
|
|
236
247
|
def render_table_frame
|
|
@@ -89,29 +89,6 @@ module Infotable
|
|
|
89
89
|
end
|
|
90
90
|
end
|
|
91
91
|
|
|
92
|
-
# Build export params with current filters, sort, and visible columns
|
|
93
|
-
def build_export_params(table_name, table_class)
|
|
94
|
-
params_hash = {
|
|
95
|
-
table_name: table_name,
|
|
96
|
-
sort_column: scalar_request_param(:sort_column),
|
|
97
|
-
sort_direction: scalar_request_param(:sort_direction),
|
|
98
|
-
sort: scalar_request_param(:sort)
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
# Add filters if present
|
|
102
|
-
if params[:filters].present?
|
|
103
|
-
params_hash[:filters] = params[:filters]
|
|
104
|
-
end
|
|
105
|
-
|
|
106
|
-
# Get visible columns from localStorage via JavaScript or use defaults
|
|
107
|
-
# For now, we'll pass all visible columns and let the controller handle it
|
|
108
|
-
# The JavaScript will need to update the link dynamically
|
|
109
|
-
visible_column_names = table_class.visible_columns.map { |c| c[:name].to_s }
|
|
110
|
-
params_hash[:visible_columns] = visible_column_names.join(",")
|
|
111
|
-
|
|
112
|
-
params_hash.compact
|
|
113
|
-
end
|
|
114
|
-
|
|
115
92
|
private
|
|
116
93
|
|
|
117
94
|
def resolve_table_class(table_name)
|
|
@@ -50,8 +50,8 @@
|
|
|
50
50
|
data-pin-direction="left"
|
|
51
51
|
title="<%= t('infotable.columns.pin.left') %>"
|
|
52
52
|
class="pin-btn pin-left w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :left ? 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300' %> transition-colors">
|
|
53
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
54
|
-
<path
|
|
53
|
+
<svg class="w-4 h-4 -rotate-45" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
54
|
+
<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>
|
|
55
55
|
</svg>
|
|
56
56
|
</button>
|
|
57
57
|
<%# Pin Right %>
|
|
@@ -61,8 +61,8 @@
|
|
|
61
61
|
data-pin-direction="right"
|
|
62
62
|
title="<%= t('infotable.columns.pin.right') %>"
|
|
63
63
|
class="pin-btn pin-right w-7 h-7 rounded flex items-center justify-center <%= column[:pinned] == :right ? 'bg-indigo-100 dark:bg-indigo-900/40 text-indigo-600 dark:text-indigo-400' : 'text-gray-400 hover:bg-gray-100 dark:hover:bg-gray-700 hover:text-gray-600 dark:hover:text-gray-300' %> transition-colors">
|
|
64
|
-
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
65
|
-
<path
|
|
64
|
+
<svg class="w-4 h-4 rotate-45" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" viewBox="0 0 24 24">
|
|
65
|
+
<path d="M12 17v5"/><path d="M9 10.76a2 2 0 0 1-1.11 1.79l-1.78.9A2 2 0 0 0 5 15.24V16a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-.76a2 2 0 0 0-1.11-1.79l-1.78-.9A2 2 0 0 1 15 10.76V7a1 1 0 0 1 1-1 2 2 0 0 0 0-4H8a2 2 0 0 0 0 4 1 1 0 0 1 1 1z"/>
|
|
66
66
|
</svg>
|
|
67
67
|
</button>
|
|
68
68
|
</div>
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<div class="border-t border-gray-100 dark:border-gray-800 bg-gradient-to-b from-gray-50/60 to-white/60 px-6 py-8 text-center">
|
|
2
|
+
<div class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
|
3
|
+
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
4
|
+
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
5
|
+
</svg>
|
|
6
|
+
<span class="font-medium"><%= t('infotable.table.loading.all_loaded') %></span>
|
|
7
|
+
</div>
|
|
8
|
+
</div>
|
|
@@ -27,13 +27,5 @@
|
|
|
27
27
|
</div>
|
|
28
28
|
<% end %>
|
|
29
29
|
<% else %>
|
|
30
|
-
|
|
31
|
-
<div class="border-t border-gray-100 dark:border-gray-800 bg-gradient-to-b from-gray-50/60 to-white/60 px-6 py-8 text-center">
|
|
32
|
-
<div class="inline-flex items-center gap-2 text-sm text-gray-500 dark:text-gray-400">
|
|
33
|
-
<svg class="h-4 w-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
|
|
34
|
-
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M5 13l4 4L19 7"></path>
|
|
35
|
-
</svg>
|
|
36
|
-
<span class="font-medium"><%= t('infotable.table.loading.all_loaded') %></span>
|
|
37
|
-
</div>
|
|
38
|
-
</div>
|
|
30
|
+
<%= render partial: "infotable/tables/end_indicator" %>
|
|
39
31
|
<% end %>
|
|
@@ -14,8 +14,15 @@
|
|
|
14
14
|
else "text-sm" # default
|
|
15
15
|
end
|
|
16
16
|
%>
|
|
17
|
-
|
|
18
|
-
|
|
17
|
+
<%
|
|
18
|
+
clickable = table_class.row_clickable?
|
|
19
|
+
row_dom_id = dom_id(record) if record.respond_to?(:to_key) && record.to_key.present?
|
|
20
|
+
row_data = table_class.row_data_for(record).transform_keys { |key| key.to_s.dasherize }
|
|
21
|
+
row_data["row-link"] = row_url if row_url
|
|
22
|
+
row_data["action"] = "click->infotable--table#handleRowClick" if clickable
|
|
23
|
+
%>
|
|
24
|
+
<tr <%= tag.attributes(id: row_dom_id, data: row_data) %>
|
|
25
|
+
class="infotable-row group relative border-t border-gray-100 dark:border-gray-800 first:border-t-0 hover:bg-gray-50 dark:hover:bg-gray-800 transition-colors duration-150 <%= 'cursor-pointer' if clickable %> <%= table_class.row_classes_for(record) %>">
|
|
19
26
|
<% if table_class.checkbox_enabled? %>
|
|
20
27
|
<td class="<%= cell_padding %> w-10">
|
|
21
28
|
<input type="checkbox"
|
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
data-default-visible="<%= column[:visible] %>"
|
|
36
36
|
<%= tag.attributes(data: { default_pinned: column[:pinned] }) if column[:pinned] %>
|
|
37
37
|
style="<%= infotable_cell_width_style(column) %>"
|
|
38
|
-
class="<%= header_padding %> text-left text-xs font-
|
|
38
|
+
class="<%= header_padding %> text-left whitespace-nowrap text-xs font-medium tracking-wide text-gray-500 dark:text-gray-400 select-none bg-gray-50 dark:bg-gray-800 sticky top-0 z-20">
|
|
39
39
|
<% if column[:sortable] %>
|
|
40
40
|
<% sort_index = current_sorts.index { |name, _| name == column[:name].to_s } %>
|
|
41
41
|
<% sort_dir = sort_index ? current_sorts[sort_index].last : nil %>
|
|
@@ -90,7 +90,7 @@
|
|
|
90
90
|
</table>
|
|
91
91
|
|
|
92
92
|
<%# Next page frame for infinite scroll - rendered after table %>
|
|
93
|
-
<% if @infinite_scroll_enabled
|
|
93
|
+
<% if @infinite_scroll_enabled %>
|
|
94
94
|
<%= render partial: "infotable/tables/next_page_frame" %>
|
|
95
95
|
<% end %>
|
|
96
96
|
<% else %>
|
data/config/importmap.rb
CHANGED
|
@@ -2,5 +2,5 @@
|
|
|
2
2
|
pin "infotable", to: "infotable/index.js"
|
|
3
3
|
pin_all_from Infotable::Engine.root.join("app/assets/javascripts/infotable/controllers"), under: "controllers/infotable", to: "infotable/controllers"
|
|
4
4
|
|
|
5
|
-
# External dependencies
|
|
6
|
-
pin "sortablejs", to: "
|
|
5
|
+
# External dependencies, served with the gem (no third-party CDN at runtime)
|
|
6
|
+
pin "sortablejs", to: "infotable/vendor/sortable.esm.js"
|
|
@@ -14,9 +14,11 @@ Infotable.configure do |config|
|
|
|
14
14
|
# Drawer width
|
|
15
15
|
config.drawer_width = "26rem" # px, rem, em, vw or %
|
|
16
16
|
|
|
17
|
-
#
|
|
18
|
-
# authentication, locale, tenant scoping etc.
|
|
19
|
-
#
|
|
17
|
+
# SECURITY: tables are served at /infotable/tables/:name/{data,export,ids}. The engine
|
|
18
|
+
# controllers inherit from this controller, so authentication, locale, tenant scoping etc.
|
|
19
|
+
# of your app also protect the tables. With the default (ActionController::Base) nothing
|
|
20
|
+
# does: anyone can read and export every table that has no `authorize { ... }` block.
|
|
21
|
+
config.parent_controller = "ApplicationController"
|
|
20
22
|
|
|
21
23
|
# Mirror search, filters, sort and page in the address bar (shareable links, Back button)
|
|
22
24
|
# config.url_state = true
|
data/lib/infotable/base.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Infotable
|
|
2
2
|
class Base
|
|
3
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, :resizable_columns_setting
|
|
4
|
+
attr_accessor :table_columns, :table_query, :table_query_sql, :pagination_settings, :sort_settings, :height_settings, :infinite_scroll_settings, :row_link_settings, :row_attr_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, :resizable_columns_setting
|
|
5
5
|
|
|
6
6
|
def inherited(subclass)
|
|
7
7
|
super
|
|
@@ -19,6 +19,7 @@ module Infotable
|
|
|
19
19
|
subclass.infinite_scroll_settings = {
|
|
20
20
|
enabled: false
|
|
21
21
|
}
|
|
22
|
+
subclass.row_attr_settings = { class: nil, data: nil }
|
|
22
23
|
subclass.row_link_settings = {
|
|
23
24
|
enabled: false,
|
|
24
25
|
url: nil
|
|
@@ -65,7 +66,9 @@ module Infotable
|
|
|
65
66
|
subclass.countless_setting = parent.countless_setting
|
|
66
67
|
subclass.url_state_setting = parent.url_state_setting
|
|
67
68
|
subclass.resizable_columns_setting = parent.resizable_columns_setting
|
|
68
|
-
|
|
69
|
+
subclass.instance_variable_set(:@toolbar_setting, parent.instance_variable_get(:@toolbar_setting))
|
|
70
|
+
subclass.instance_variable_set(:@export_column_names, parent.instance_variable_get(:@export_column_names)&.dup)
|
|
71
|
+
%i[pagination_settings sort_settings height_settings infinite_scroll_settings row_link_settings row_attr_settings
|
|
69
72
|
slim_rows_settings row_size_settings filters_button_settings display_settings
|
|
70
73
|
nil_value_settings empty_value_settings].each do |attr|
|
|
71
74
|
subclass.public_send("#{attr}=", parent.public_send(attr).dup)
|
|
@@ -134,8 +137,9 @@ module Infotable
|
|
|
134
137
|
# Plucks when the id method is a plain column of an ungrouped query, loads records otherwise.
|
|
135
138
|
def checkbox_ids(query, limit = nil)
|
|
136
139
|
method = @checkbox_settings[:id_method]
|
|
137
|
-
|
|
138
|
-
|
|
140
|
+
# Without a cap the order is irrelevant (and costs a sort); with one, the table's own order
|
|
141
|
+
# decides which rows make it in, so a capped selection is the first N rows the user sees
|
|
142
|
+
scope = limit ? query.limit(limit) : query.reorder(nil)
|
|
139
143
|
model = query.model
|
|
140
144
|
|
|
141
145
|
if model.column_names.include?(method.to_s) && scope.group_values.empty? && scope.having_clause.empty?
|
|
@@ -167,8 +171,16 @@ module Infotable
|
|
|
167
171
|
end
|
|
168
172
|
|
|
169
173
|
# Define a column
|
|
174
|
+
COLUMN_OPTIONS = %i[label type sortable searchable filterable visible exportable pinned virtual virtual_sql
|
|
175
|
+
sql sort_sql value includes formatter formatter_options actions filter_type filter_options
|
|
176
|
+
filter_options_cache filter_options_limit filter_multi_select width].freeze
|
|
177
|
+
|
|
170
178
|
def column(name, **options)
|
|
171
179
|
@table_columns ||= []
|
|
180
|
+
unknown = options.keys - COLUMN_OPTIONS
|
|
181
|
+
if unknown.any? && defined?(Rails) && Rails.logger
|
|
182
|
+
Rails.logger.warn("[Infotable] #{self.name || 'table'}: column :#{name} ignores unknown option(s) #{unknown.map(&:inspect).join(', ')}")
|
|
183
|
+
end
|
|
172
184
|
type = options[:type] || :string
|
|
173
185
|
|
|
174
186
|
# Determine width - use custom if provided and not blank, otherwise use default
|
|
@@ -319,6 +331,51 @@ module Infotable
|
|
|
319
331
|
end
|
|
320
332
|
end
|
|
321
333
|
|
|
334
|
+
# Extra CSS classes per row: a String, or a Proc that receives the record (nil/false adds nothing).
|
|
335
|
+
# Tailwind only generates classes it can see in your source, so write them out in full.
|
|
336
|
+
# @example
|
|
337
|
+
# row_class ->(order) { "bg-red-50" if order.overdue? }
|
|
338
|
+
def row_class(value = nil, &block)
|
|
339
|
+
@row_attr_settings[:class] = value || block
|
|
340
|
+
end
|
|
341
|
+
|
|
342
|
+
def row_classes_for(record)
|
|
343
|
+
setting = @row_attr_settings[:class]
|
|
344
|
+
(setting.respond_to?(:call) ? setting.call(record) : setting).presence
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
# Extra data-* attributes per row: a Proc that receives the record and returns a Hash
|
|
348
|
+
# ({ post_id: 1 } becomes data-post-id="1"). Handy for your own Stimulus controllers.
|
|
349
|
+
# @example
|
|
350
|
+
# row_data ->(post) { { post_id: post.id, status: post.status } }
|
|
351
|
+
def row_data(value = nil, &block)
|
|
352
|
+
@row_attr_settings[:data] = value || block
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def row_data_for(record)
|
|
356
|
+
setting = @row_attr_settings[:data]
|
|
357
|
+
data = setting.respond_to?(:call) ? setting.call(record) : setting
|
|
358
|
+
data.is_a?(Hash) ? data : {}
|
|
359
|
+
end
|
|
360
|
+
|
|
361
|
+
# Clicking a row (not a link, button or input inside it) fires a cancelable
|
|
362
|
+
# `infotable:row-click` event on the row (detail: { id, url, data }). If nothing calls
|
|
363
|
+
# preventDefault() and the table has a row_link, the click falls back to following it.
|
|
364
|
+
# @example
|
|
365
|
+
# row_click true
|
|
366
|
+
def row_click(enabled = true)
|
|
367
|
+
@row_link_settings[:click] = enabled ? true : false
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def row_click?
|
|
371
|
+
@row_link_settings[:click] ? true : false
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
# Rows react to clicks (row_link and/or row_click)
|
|
375
|
+
def row_clickable?
|
|
376
|
+
row_link_enabled? || row_click?
|
|
377
|
+
end
|
|
378
|
+
|
|
322
379
|
# Set row size (padding and text size)
|
|
323
380
|
# @param size [Symbol] - Row size (:slim, :mid, :default)
|
|
324
381
|
# @example
|
|
@@ -31,7 +31,7 @@ module Infotable
|
|
|
31
31
|
@nil_value = "-" # Default placeholder for nil values
|
|
32
32
|
@empty_value = "-" # Default placeholder for empty strings
|
|
33
33
|
@url_state = true # mirror table state in the address bar
|
|
34
|
-
@max_select_all =
|
|
34
|
+
@max_select_all = 4000 # "select all matching" collects at most this many ids (nil = no limit). Rack accepts 4096 params per request, and every id is sent as ids[]
|
|
35
35
|
@max_filter_options = 500 # select filters render at most this many <option>s (nil = no limit)
|
|
36
36
|
@parent_controller = "ActionController::Base" # Set to "ApplicationController" to inherit host auth/filters
|
|
37
37
|
end
|
data/lib/infotable/engine.rb
CHANGED
|
@@ -6,18 +6,11 @@ module Infotable
|
|
|
6
6
|
class Engine < ::Rails::Engine
|
|
7
7
|
isolate_namespace Infotable
|
|
8
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
9
|
# Rails already adds the engine's app/assets/* directories to the asset path;
|
|
17
10
|
# only the precompile list is needed. Skipped for apps without an asset pipeline.
|
|
18
11
|
initializer "infotable.assets" do |app|
|
|
19
12
|
if app.config.respond_to?(:assets)
|
|
20
|
-
app.config.assets.precompile += %w[infotable/controllers/*.js]
|
|
13
|
+
app.config.assets.precompile += %w[infotable/controllers/*.js infotable/vendor/*.js]
|
|
21
14
|
end
|
|
22
15
|
end
|
|
23
16
|
|
|
@@ -110,7 +110,7 @@ module Infotable
|
|
|
110
110
|
column_name = column[:name]
|
|
111
111
|
is_virtual = having?(column)
|
|
112
112
|
# Use qualified column reference
|
|
113
|
-
column_ref =
|
|
113
|
+
column_ref = text_ref(column)
|
|
114
114
|
|
|
115
115
|
result = query
|
|
116
116
|
|
|
@@ -145,6 +145,14 @@ module Infotable
|
|
|
145
145
|
result
|
|
146
146
|
end
|
|
147
147
|
|
|
148
|
+
# PostgreSQL has no LOWER / ILIKE / = '' for numbers, dates, uuids...: a text filter on such a
|
|
149
|
+
# column (filter_type: :text on an integer) compares its text form.
|
|
150
|
+
def text_ref(column)
|
|
151
|
+
ref = qualified_column_ref(column)
|
|
152
|
+
type = select_column_type(column)
|
|
153
|
+
type.nil? || %i[string text].include?(type) ? ref : "CAST(#{ref} AS text)"
|
|
154
|
+
end
|
|
155
|
+
|
|
148
156
|
# Boolean filter
|
|
149
157
|
def apply_boolean_filter(column, params)
|
|
150
158
|
return query unless params.is_a?(Hash)
|
|
@@ -245,6 +253,7 @@ module Infotable
|
|
|
245
253
|
from = parse_date(scalar(params[:from] || params["from"]))
|
|
246
254
|
to = parse_date(scalar(params[:to] || params["to"]))
|
|
247
255
|
to = to.end_of_day if to && datetime
|
|
256
|
+
from = from.beginning_of_day if from && datetime # same zone as `to` (Time.zone), not UTC midnight
|
|
248
257
|
|
|
249
258
|
result = query
|
|
250
259
|
result = result.public_send(filter_method, "#{column_ref} >= ?", from) if from
|
|
@@ -10,7 +10,7 @@ module Infotable
|
|
|
10
10
|
when :badge
|
|
11
11
|
format_as_badge(value, true_text, false_text)
|
|
12
12
|
when :icon
|
|
13
|
-
format_as_icon(value)
|
|
13
|
+
format_as_icon(value, true_text, false_text)
|
|
14
14
|
else
|
|
15
15
|
value ? true_text : false_text
|
|
16
16
|
end
|
|
@@ -26,11 +26,11 @@ module Infotable
|
|
|
26
26
|
end
|
|
27
27
|
end
|
|
28
28
|
|
|
29
|
-
def format_as_icon(value)
|
|
29
|
+
def format_as_icon(value, true_text, false_text)
|
|
30
30
|
if value
|
|
31
|
-
html_safe('<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>')
|
|
31
|
+
html_safe('<svg class="w-5 h-5 text-green-500" fill="currentColor" viewBox="0 0 20 20" role="img" aria-label="' + ERB::Util.html_escape(true_text) + '"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-9.293a1 1 0 00-1.414-1.414L9 10.586 7.707 9.293a1 1 0 00-1.414 1.414l2 2a1 1 0 001.414 0l4-4z" clip-rule="evenodd"/></svg>')
|
|
32
32
|
else
|
|
33
|
-
html_safe('<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>')
|
|
33
|
+
html_safe('<svg class="w-5 h-5 text-gray-400" fill="currentColor" viewBox="0 0 20 20" role="img" aria-label="' + ERB::Util.html_escape(false_text) + '"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zM8.707 7.293a1 1 0 00-1.414 1.414L8.586 10l-1.293 1.293a1 1 0 101.414 1.414L10 11.414l1.293 1.293a1 1 0 001.414-1.414L11.414 10l1.293-1.293a1 1 0 00-1.414-1.414L10 8.586 8.707 7.293z" clip-rule="evenodd"/></svg>')
|
|
34
34
|
end
|
|
35
35
|
end
|
|
36
36
|
end
|
|
@@ -22,9 +22,18 @@ module Infotable
|
|
|
22
22
|
raise NotFound, "Table '#{table_name}' not found. Make sure it's defined in app/infotable/"
|
|
23
23
|
end
|
|
24
24
|
|
|
25
|
+
CANDIDATES = Concurrent::Map.new
|
|
26
|
+
|
|
27
|
+
# The class names to try are pure string work (up to 2**(n-1) of them): memoized per name
|
|
25
28
|
def candidates(table_name)
|
|
26
29
|
return [] unless table_name.to_s.match?(/\A[a-z0-9]+(_[a-z0-9]+)*\z/i)
|
|
27
30
|
|
|
31
|
+
CANDIDATES.compute_if_absent(table_name.to_s) { build_candidates(table_name) }
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def build_candidates(table_name)
|
|
35
|
+
return [] unless table_name.to_s.match?(/\A[a-z0-9]+(_[a-z0-9]+)*\z/i)
|
|
36
|
+
|
|
28
37
|
parts = table_name.to_s.sub(/_table\z/, "").split("_")
|
|
29
38
|
return [] if parts.empty? || parts.size > MAX_PARTS
|
|
30
39
|
|
data/lib/infotable/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: infotable
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 1.
|
|
4
|
+
version: 1.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Jakub Cieślar
|
|
@@ -137,6 +137,7 @@ files:
|
|
|
137
137
|
- app/assets/javascripts/infotable/controllers/drawer_controller.js
|
|
138
138
|
- app/assets/javascripts/infotable/controllers/table_controller.js
|
|
139
139
|
- app/assets/javascripts/infotable/index.js
|
|
140
|
+
- app/assets/javascripts/infotable/vendor/sortable.esm.js
|
|
140
141
|
- app/assets/tailwind/infotable/engine.css
|
|
141
142
|
- app/controllers/infotable/application_controller.rb
|
|
142
143
|
- app/controllers/infotable/tables_controller.rb
|
|
@@ -147,6 +148,7 @@ files:
|
|
|
147
148
|
- app/models/infotable/application_record.rb
|
|
148
149
|
- app/views/infotable/tables/_active_filters.html.erb
|
|
149
150
|
- app/views/infotable/tables/_column_visibility.html.erb
|
|
151
|
+
- app/views/infotable/tables/_end_indicator.html.erb
|
|
150
152
|
- app/views/infotable/tables/_filter_drawer.html.erb
|
|
151
153
|
- app/views/infotable/tables/_next_page_frame.html.erb
|
|
152
154
|
- app/views/infotable/tables/_pagination.html.erb
|