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.
Files changed (58) hide show
  1. checksums.yaml +7 -0
  2. data/MIT-LICENSE +20 -0
  3. data/README.md +1043 -0
  4. data/Rakefile +6 -0
  5. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +1412 -0
  6. data/app/assets/javascripts/infotable/controllers/table_controller.js +921 -0
  7. data/app/assets/javascripts/infotable/index.js +14 -0
  8. data/app/controllers/infotable/application_controller.rb +4 -0
  9. data/app/controllers/infotable/tables_controller.rb +299 -0
  10. data/app/helpers/infotable/application_helper.rb +5 -0
  11. data/app/helpers/infotable/tables_helper.rb +354 -0
  12. data/app/jobs/infotable/application_job.rb +4 -0
  13. data/app/mailers/infotable/application_mailer.rb +6 -0
  14. data/app/models/infotable/application_record.rb +5 -0
  15. data/app/views/infotable/tables/_column_visibility.html.erb +100 -0
  16. data/app/views/infotable/tables/_filter_drawer.html.erb +65 -0
  17. data/app/views/infotable/tables/_next_page_frame.html.erb +39 -0
  18. data/app/views/infotable/tables/_pagination.html.erb +101 -0
  19. data/app/views/infotable/tables/_side_panel.html.erb +63 -0
  20. data/app/views/infotable/tables/_table.html.erb +135 -0
  21. data/app/views/infotable/tables/_table_rows.html.erb +38 -0
  22. data/app/views/infotable/tables/filters/_boolean.html.erb +22 -0
  23. data/app/views/infotable/tables/filters/_date.html.erb +30 -0
  24. data/app/views/infotable/tables/filters/_number.html.erb +32 -0
  25. data/app/views/infotable/tables/filters/_select.html.erb +68 -0
  26. data/app/views/infotable/tables/filters/_text.html.erb +38 -0
  27. data/app/views/infotable/tables/show.html.erb +113 -0
  28. data/app/views/layouts/infotable/application.html.erb +15 -0
  29. data/config/importmap.rb +6 -0
  30. data/config/locales/en.yml +88 -0
  31. data/config/locales/pl.yml +88 -0
  32. data/config/routes.rb +6 -0
  33. data/lib/generators/infotable/install/install_generator.rb +565 -0
  34. data/lib/generators/infotable/install/templates/TAILWIND_CONFIG.md +131 -0
  35. data/lib/generators/infotable/install/templates/initializer.rb +26 -0
  36. data/lib/generators/infotable/install/templates/tailwind.config.js +284 -0
  37. data/lib/generators/infotable/table/table_generator.rb +95 -0
  38. data/lib/generators/infotable/table/templates/table.rb.tt +26 -0
  39. data/lib/infotable/base.rb +463 -0
  40. data/lib/infotable/batch_actions.rb +34 -0
  41. data/lib/infotable/configuration.rb +53 -0
  42. data/lib/infotable/engine.rb +51 -0
  43. data/lib/infotable/filter_processor.rb +326 -0
  44. data/lib/infotable/formatters/action_formatter.rb +141 -0
  45. data/lib/infotable/formatters/avatar_formatter.rb +49 -0
  46. data/lib/infotable/formatters/badge_formatter.rb +80 -0
  47. data/lib/infotable/formatters/base_formatter.rb +47 -0
  48. data/lib/infotable/formatters/boolean_formatter.rb +38 -0
  49. data/lib/infotable/formatters/currency_formatter.rb +22 -0
  50. data/lib/infotable/formatters/date_formatter.rb +58 -0
  51. data/lib/infotable/formatters/link_formatter.rb +48 -0
  52. data/lib/infotable/formatters/percent_formatter.rb +27 -0
  53. data/lib/infotable/search_processor.rb +42 -0
  54. data/lib/infotable/table_resolver.rb +45 -0
  55. data/lib/infotable/version.rb +3 -0
  56. data/lib/infotable.rb +54 -0
  57. data/lib/tasks/infotable.rake +55 -0
  58. metadata +216 -0
@@ -0,0 +1,26 @@
1
+ # InfoTable Configuration
2
+ # For more configuration options, see: https://github.com/jcieslar/infotable
3
+
4
+ Infotable.configure do |config|
5
+ # Default number of items per page
6
+ config.default_per_page = 25
7
+
8
+ # Available per-page options
9
+ config.per_page_options = [ 10, 25, 50, 100 ]
10
+
11
+ # Filter drawer position (:left or :right)
12
+ config.drawer_position = :right
13
+
14
+ # Drawer width
15
+ config.drawer_width = "26rem" # px, rem, em, vw or %
16
+
17
+ # Controller the engine controllers inherit from. Use your app's controller so
18
+ # authentication, locale, tenant scoping etc. also protect the tables:
19
+ # config.parent_controller = "ApplicationController"
20
+
21
+ # Mirror search, filters, sort and page in the address bar (shareable links, Back button)
22
+ # config.url_state = true
23
+
24
+ # Filter behavior: true = auto-apply filters (debounced, no Apply button), false = manual apply button
25
+ config.autofilter = true
26
+ end
@@ -0,0 +1,284 @@
1
+ const { execSync } = require('child_process');
2
+ const path = require('path');
3
+
4
+ // Helper to resolve gem path dynamically (works with path:, git:, and rubygems)
5
+ function resolveGemPath(gemName) {
6
+ try {
7
+ const bundlerCmd = `bundle show ${gemName}`;
8
+ const gemPath = execSync(bundlerCmd, { encoding: 'utf-8' }).trim();
9
+ return gemPath;
10
+ } catch (error) {
11
+ console.warn(`Warning: Could not resolve ${gemName} gem path:`, error.message);
12
+ return null;
13
+ }
14
+ }
15
+
16
+ const infotableGemPath = resolveGemPath('infotable');
17
+
18
+ // Build content paths
19
+ const contentPaths = [
20
+ './app/javascript/**/*.js',
21
+ './app/**/*.html.erb',
22
+ './app/helpers/**/*.rb',
23
+ './app/infotable/**/*.rb'
24
+ ];
25
+
26
+ // Add infotable gem views if gem path is resolved
27
+ if (infotableGemPath) {
28
+ contentPaths.push(path.join(infotableGemPath, 'app/views/infotable/**/*.{erb,rb}'));
29
+ contentPaths.push(path.join(infotableGemPath, 'app/assets/javascripts/infotable/**/*.js'));
30
+ contentPaths.push(path.join(infotableGemPath, 'lib/infotable/formatters/**/*.rb'));
31
+ contentPaths.push(path.join(infotableGemPath, 'app/components/**/*.rb'));
32
+ contentPaths.push(path.join(infotableGemPath, 'app/components/**/*.html.erb'));
33
+ }
34
+
35
+ module.exports = {
36
+ content: contentPaths,
37
+ theme: {
38
+ extend: {
39
+ fontFamily: {
40
+ sans: [
41
+ 'Nunito',
42
+ 'ui-sans-serif',
43
+ 'system-ui',
44
+ '-apple-system',
45
+ 'BlinkMacSystemFont',
46
+ 'Segoe UI',
47
+ 'Roboto',
48
+ 'Helvetica Neue',
49
+ 'Arial',
50
+ 'Noto Sans',
51
+ 'sans-serif',
52
+ 'Apple Color Emoji',
53
+ 'Segoe UI Emoji',
54
+ 'Segoe UI Symbol',
55
+ 'Noto Color Emoji',
56
+ ],
57
+ },
58
+ colors: {
59
+ transparent: 'transparent',
60
+ current: 'currentColor',
61
+
62
+ black: '#000',
63
+ white: '#fff',
64
+
65
+ purple: {
66
+ 50: '#DBCDDD',
67
+ 100: '#C3AABE',
68
+ 200: '#A787A1',
69
+ 300: '#9D6795',
70
+ 400: '#895482',
71
+ 500: '#7F4677',
72
+ 600: '#742669',
73
+ 700: '#691B5E',
74
+ 800: '#58114E',
75
+ 900: '#47023D',
76
+ },
77
+ primary: {
78
+ 50: '#F7F5FA',
79
+ 100: '#F0EAF6',
80
+ 200: '#D7C6E3',
81
+ 300: '#A887B5',
82
+ 400: '#835696',
83
+ 500: '#733982',
84
+ 600: '#701A75',
85
+ 700: '#5F1568',
86
+ 800: '#510761',
87
+ 900: '#4A044E',
88
+ },
89
+
90
+ blue: {
91
+ 50: '#E9F3F9',
92
+ 100: '#D8E9F2',
93
+ 200: '#C0DAE8',
94
+ 300: '#9ECDE6',
95
+ 400: '#63ADD4',
96
+ 500: '#2B8ABD',
97
+ 600: '#006DA7',
98
+ 700: '#075985',
99
+ 800: '#004164',
100
+ 900: '#002E47',
101
+ },
102
+ silver: {
103
+ 50: '#F6F8FA',
104
+ 100: '#F0F2F4',
105
+ 200: '#EAEDF0',
106
+ 300: '#E2E5E9',
107
+ 400: '#D0D8E1',
108
+ 500: '#C4CED9',
109
+ 600: '#B6BFCA',
110
+ 700: '#94A4B7',
111
+ 800: '#6A7D93',
112
+ 900: '#4D5F74',
113
+ },
114
+ gray: {
115
+ 50: '#F6F6F6',
116
+ 100: '#EFEFF0',
117
+ 200: '#E4E4E7',
118
+ 300: '#D7D7D7',
119
+ 400: '#CBCACA',
120
+ 500: '#AFAFAF',
121
+ 600: '#8C8C8C',
122
+ 700: '#6B6B6B',
123
+ 800: '#555555',
124
+ 900: '#3B3B3B',
125
+ },
126
+ 'rating-green': {
127
+ 50: '#F3F8EC',
128
+ 100: '#E5F5CF',
129
+ 200: '#D2E9B4',
130
+ 300: '#C2DC9E',
131
+ 400: '#A9CE78',
132
+ 500: '#83B83D',
133
+ 600: '#53A925',
134
+ 700: '#04812A',
135
+ 800: '#006332',
136
+ 900: '#064E31',
137
+ },
138
+ pink: {
139
+ 50: '#F6EDF3',
140
+ 100: '#EAD1E0',
141
+ 200: '#D7A3C2',
142
+ 300: '#D078AD',
143
+ 400: '#D060A3',
144
+ 500: '#BD438C',
145
+ 600: '#AC2C78',
146
+ 700: '#9C0B62',
147
+ 800: '#880353',
148
+ 900: '#6B0040',
149
+ },
150
+ 'rating-red': {
151
+ 50: '#FEF2F2',
152
+ 100: '#FEE2E2',
153
+ 200: '#FECACA',
154
+ 300: '#FCA5A5',
155
+ 400: '#F87171',
156
+ 500: '#EF444D',
157
+ 600: '#E31C37',
158
+ 700: '#BF1229',
159
+ 800: '#990317',
160
+ 900: '#7D0011',
161
+ },
162
+ red: {
163
+ 50: '#FFF5F3',
164
+ 100: '#FDE8E5',
165
+ 200: '#FCD9D3',
166
+ 300: '#FCBEB3',
167
+ 400: '#F99280',
168
+ 500: '#DD655A',
169
+ 600: '#D7443D',
170
+ 700: '#BD3730',
171
+ 800: '#A41008',
172
+ 900: '#710400',
173
+ },
174
+ yellow: {
175
+ 50: '#FFFBEB',
176
+ 100: '#FEF3C7',
177
+ 200: '#FDE68A',
178
+ 300: '#FCD34D',
179
+ 400: '#FBBF24',
180
+ 500: '#F59E0B',
181
+ 600: '#D97706',
182
+ 700: '#AA5B0B',
183
+ 800: '#924900',
184
+ 900: '#763B00',
185
+ },
186
+
187
+ gold: {
188
+ 50: '#FDFAF4',
189
+ 100: '#FFFAE9',
190
+ 200: '#F8EEDA',
191
+ 300: '#EFDCB4',
192
+ 400: '#E5C582',
193
+ 500: '#EDB745',
194
+ 600: '#E7A212',
195
+ 700: '#CA8A04',
196
+ 800: '#A16207',
197
+ 900: '#854D0E',
198
+ },
199
+
200
+ 'rating-yellow': {
201
+ 50: '#FEF7ED',
202
+ 100: '#FBECD5',
203
+ 200: '#FDE3BA',
204
+ 300: '#F4D199',
205
+ 400: '#EFBA66',
206
+ 500: '#E9A333',
207
+ 600: '#E48D00',
208
+ 700: '#C86400',
209
+ 800: '#B15200',
210
+ 900: '#854D0E',
211
+ },
212
+
213
+ bronze: {
214
+ 50: '#FAFAF9',
215
+ 100: '#F6F4F3',
216
+ 200: '#F1EFEF',
217
+ 300: '#ECEAE9',
218
+ 400: '#E5DFDF',
219
+ 500: '#CEC3C0',
220
+ 600: '#B8A8A4',
221
+ 700: '#B1938C',
222
+ 800: '#96736A',
223
+ 900: '#825E55',
224
+ },
225
+
226
+ orange: {
227
+ 50: '#FEF0E6',
228
+ 100: '#FCE7D6',
229
+ 200: '#F9CEAD',
230
+ 300: '#F6AA71',
231
+ 400: '#F18533',
232
+ 500: '#EE6600',
233
+ 600: '#D75E02',
234
+ 700: '#B14C00',
235
+ 800: '#883B00',
236
+ 900: '#672C00',
237
+ },
238
+
239
+ green: {
240
+ 50: '#EDF7F2',
241
+ 100: '#D3EADF',
242
+ 200: '#B1D7C5',
243
+ 300: '#87BCA3',
244
+ 400: '#5CA482',
245
+ 500: '#399175',
246
+ 600: '#267767',
247
+ 700: '#0A635D',
248
+ 800: '#1E4443',
249
+ 900: '#072120',
250
+ },
251
+
252
+ tags: {
253
+ 50: '#E8EFF2',
254
+ 100: '#C1D8DE',
255
+ 200: '#A1BFC8',
256
+ 300: '#739EAC',
257
+ 400: '#3E90A6',
258
+ 500: '#0E7490',
259
+ 600: '#155E75',
260
+ 700: '#164E63',
261
+ 800: '#174253',
262
+ 900: '#002A3A',
263
+ },
264
+ 'gray-purple': {
265
+ 50: '#F1F0F5',
266
+ 100: '#E2E0EA',
267
+ 200: '#D0CCE4',
268
+ 300: '#BFBBD1',
269
+ 400: '#B3A4C7',
270
+ 500: '#A082C9',
271
+ 600: '#8459A6',
272
+ 700: '#5E3181',
273
+ 800: '#2D1554',
274
+ 900: '#201633',
275
+ },
276
+ },
277
+ },
278
+ },
279
+ plugins: [
280
+ require('@tailwindcss/forms')({
281
+ strategy: 'class',
282
+ }),
283
+ ],
284
+ }
@@ -0,0 +1,95 @@
1
+ module Infotable
2
+ module Generators
3
+ # rails generate infotable:table User
4
+ # rails generate infotable:table Admin::Order --only=id,number,status --skip=notes
5
+ class TableGenerator < Rails::Generators::NamedBase
6
+ source_root File.expand_path("templates", __dir__)
7
+
8
+ desc "Create an InfoTable definition in app/infotable from a model's columns"
9
+
10
+ class_option :only, type: :array, default: [], desc: "Only these columns"
11
+ class_option :skip, type: :array, default: [], desc: "Leave these columns out"
12
+
13
+ # Attributes that must never end up in a table by accident
14
+ SENSITIVE = /password|digest|token|secret|otp|encrypted/
15
+
16
+ TYPE_MAP = {
17
+ string: { type: :string, filter: :text },
18
+ text: { type: :text, filter: :text },
19
+ integer: { type: :integer, filter: :number },
20
+ bigint: { type: :integer, filter: :number },
21
+ float: { type: :float, filter: :number },
22
+ decimal: { type: :decimal, filter: :number },
23
+ boolean: { type: :boolean, filter: :boolean },
24
+ date: { type: :date, filter: :date },
25
+ datetime: { type: :datetime, filter: :date },
26
+ timestamp: { type: :datetime, filter: :date },
27
+ time: { type: :string, filter: :text }
28
+ }.freeze
29
+
30
+ def create_table_file
31
+ @model = model_class
32
+ say_status :warning, "#{class_name} not found; generating an empty table to fill in", :yellow unless @model
33
+
34
+ template "table.rb.tt", File.join("app/infotable", class_path, "#{table_file_name}.rb")
35
+ end
36
+
37
+ private
38
+
39
+ def table_file_name
40
+ "#{file_name.pluralize}_table"
41
+ end
42
+
43
+ def table_class_name
44
+ (class_path.map(&:camelize) + [ "#{file_name.pluralize.camelize}Table" ]).join("::")
45
+ end
46
+
47
+ def model_class
48
+ class_name.safe_constantize
49
+ end
50
+
51
+ # Column definitions as hashes: name, type, filter, searchable, select options
52
+ def column_specs
53
+ return [] unless @model.respond_to?(:columns)
54
+
55
+ foreign_keys = @model.respond_to?(:reflect_on_all_associations) ? @model.reflect_on_all_associations(:belongs_to).map { _1.foreign_key.to_s } : []
56
+
57
+ @model.columns.filter_map do |column|
58
+ name = column.name
59
+ next if SENSITIVE.match?(name)
60
+ next if foreign_keys.include?(name)
61
+ next if options[:only].any? && options[:only].exclude?(name)
62
+ next if options[:skip].include?(name)
63
+
64
+ spec = TYPE_MAP[column.type] || { type: :string, filter: :text }
65
+ enum_values = @model.respond_to?(:defined_enums) ? @model.defined_enums[name]&.keys : nil
66
+
67
+ {
68
+ name: name,
69
+ type: enum_values ? :badge : spec[:type],
70
+ filter: enum_values ? :select : spec[:filter],
71
+ enum_values: enum_values,
72
+ searchable: %i[string text].include?(column.type) && enum_values.nil?,
73
+ primary_key: name == @model.primary_key.to_s
74
+ }
75
+ end
76
+ end
77
+
78
+ # belongs_to associations become commented hints: they need a join the app has to choose
79
+ def association_hints
80
+ return [] unless @model.respond_to?(:reflect_on_all_associations)
81
+
82
+ @model.reflect_on_all_associations(:belongs_to).map(&:name)
83
+ end
84
+
85
+ def default_sort_line
86
+ names = column_specs.map { _1[:name] }
87
+ if names.include?("created_at")
88
+ "default_sort :created_at, :desc"
89
+ elsif names.any?
90
+ "default_sort :#{names.first}, :asc"
91
+ end
92
+ end
93
+ end
94
+ end
95
+ end
@@ -0,0 +1,26 @@
1
+ class <%= table_class_name %> < Infotable::Base
2
+ query { <%= class_name %>.all }<%= " # TODO: #{class_name} was not found, point this at your model" unless model_class %>
3
+ <% if column_specs.empty? %>
4
+
5
+ # column :name, label: "Name", type: :string, sortable: true, searchable: true, filterable: true, filter_type: :text
6
+ <% end %>
7
+ <% column_specs.each do |spec| -%>
8
+
9
+ column :<%= spec[:name] %>,
10
+ type: :<%= spec[:type] %>,
11
+ sortable: true<%= ",\n searchable: true" if spec[:searchable] %>,
12
+ filterable: <%= spec[:primary_key] ? "false" : "true" %><%= ",\n filter_type: :#{spec[:filter]}" unless spec[:primary_key] %><%= ",\n filter_options: %w[#{spec[:enum_values].join(' ')}]" if spec[:enum_values] %><%= ",\n visible: false" if spec[:primary_key] %>
13
+ <% end -%>
14
+ <% if association_hints.any? %>
15
+
16
+ # Associations: show them with a join, e.g.
17
+ <% association_hints.each do |association| -%>
18
+ # column :<%= association %>, sql: "<%= association.to_s.pluralize %>.name", includes: :<%= association %>, value: ->(record) { record.<%= association %>&.name }
19
+ <% end -%>
20
+ <% end -%>
21
+
22
+ per_page 25
23
+ <% if default_sort_line %>
24
+ <%= default_sort_line %>
25
+ <% end -%>
26
+ end