infotable 1.4.1 → 1.6.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 (54) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +179 -11
  3. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +74 -378
  4. data/app/assets/javascripts/infotable/controllers/table_controller.js +112 -120
  5. data/app/assets/javascripts/infotable/vendor/sortable.esm.js +3363 -0
  6. data/app/controllers/infotable/tables_controller.rb +16 -5
  7. data/app/helpers/infotable/tables_helper.rb +10 -48
  8. data/app/views/infotable/tables/_column_visibility.html.erb +4 -4
  9. data/app/views/infotable/tables/_end_indicator.html.erb +8 -0
  10. data/app/views/infotable/tables/_filter_drawer.html.erb +6 -6
  11. data/app/views/infotable/tables/_next_page_frame.html.erb +1 -9
  12. data/app/views/infotable/tables/_pagination.html.erb +4 -7
  13. data/app/views/infotable/tables/_table.html.erb +1 -1
  14. data/app/views/infotable/tables/_table_rows.html.erb +9 -2
  15. data/app/views/infotable/tables/filters/_boolean.html.erb +1 -1
  16. data/app/views/infotable/tables/filters/_range.html.erb +1 -1
  17. data/app/views/infotable/tables/filters/_select.html.erb +2 -2
  18. data/app/views/infotable/tables/filters/_text.html.erb +2 -2
  19. data/app/views/infotable/tables/show.html.erb +2 -2
  20. data/config/importmap.rb +2 -2
  21. data/config/locales/de.yml +126 -0
  22. data/config/locales/el.yml +126 -0
  23. data/config/locales/en.yml +18 -4
  24. data/config/locales/es.yml +126 -0
  25. data/config/locales/fr.yml +126 -0
  26. data/config/locales/it.yml +126 -0
  27. data/config/locales/pl.yml +18 -4
  28. data/exe/infotable-mcp +13 -0
  29. data/lib/generators/infotable/install/templates/initializer.rb +5 -3
  30. data/lib/generators/infotable/table/table_generator.rb +23 -1
  31. data/lib/generators/infotable/table/templates/table.rb.tt +8 -1
  32. data/lib/infotable/base.rb +62 -4
  33. data/lib/infotable/configuration.rb +1 -1
  34. data/lib/infotable/engine.rb +1 -8
  35. data/lib/infotable/filter_processor.rb +10 -1
  36. data/lib/infotable/formatters/boolean_formatter.rb +4 -4
  37. data/lib/infotable/formatters/currency_formatter.rb +4 -12
  38. data/lib/infotable/formatters/date_formatter.rb +3 -9
  39. data/lib/infotable/formatters/duration_formatter.rb +51 -0
  40. data/lib/infotable/formatters/filesize_formatter.rb +14 -0
  41. data/lib/infotable/formatters/image_formatter.rb +31 -0
  42. data/lib/infotable/formatters/number_formatter.rb +46 -0
  43. data/lib/infotable/formatters/tags_formatter.rb +44 -0
  44. data/lib/infotable/formatters/truncate_formatter.rb +19 -0
  45. data/lib/infotable/mcp/introspection.rb +94 -0
  46. data/lib/infotable/mcp/server.rb +26 -0
  47. data/lib/infotable/mcp/table_checker.rb +80 -0
  48. data/lib/infotable/mcp/table_writer.rb +66 -0
  49. data/lib/infotable/mcp/tools.rb +122 -0
  50. data/lib/infotable/mcp.rb +30 -0
  51. data/lib/infotable/table_resolver.rb +9 -0
  52. data/lib/infotable/version.rb +1 -1
  53. data/lib/infotable.rb +22 -0
  54. metadata +53 -4
@@ -0,0 +1,66 @@
1
+ require "generators/infotable/table/table_generator"
2
+
3
+ module Infotable
4
+ module Mcp
5
+ # Renders a table with the `infotable:table` generator into a scratch directory,
6
+ # so the code can be shown first and written to app/infotable only on request.
7
+ class TableWriter
8
+ Result = Struct.new(:path, :code, keyword_init: true)
9
+
10
+ class Error < StandardError; end
11
+
12
+ FLAGS = %i[only skip per_page sort toolbar row_click infinite_scroll countless checkbox authorize].freeze
13
+
14
+ def initialize(model, settings = {})
15
+ @model = model.to_s
16
+ @settings = settings.to_h.symbolize_keys.slice(*FLAGS).compact
17
+ end
18
+
19
+ def render
20
+ raise Error, "unknown model #{@model}" unless @model.safe_constantize.is_a?(Class)
21
+
22
+ Dir.mktmpdir("infotable-mcp") do |dir|
23
+ shell = Thor::Base.shell.new
24
+ generator = Infotable::Generators::TableGenerator.new([ @model ], switches, destination_root: dir, shell: shell)
25
+ shell.mute { generator.invoke_all }
26
+
27
+ file = Dir.glob("app/infotable/**/*.rb", base: dir).first or raise Error, "the generator wrote nothing"
28
+ code = File.read(File.join(dir, file))
29
+ check_syntax!(code)
30
+ Result.new(path: file, code: code)
31
+ end
32
+ end
33
+
34
+ # Writes the rendered table into the app; an existing file is only replaced with overwrite: true
35
+ def write(overwrite: false)
36
+ result = render
37
+ target = Rails.root.join(result.path)
38
+ raise Error, "#{result.path} already exists, pass overwrite: true to replace it" if target.exist? && !overwrite
39
+
40
+ target.dirname.mkpath
41
+ target.write(result.code)
42
+ result
43
+ end
44
+
45
+ private
46
+
47
+ def switches
48
+ @settings.flat_map do |key, value|
49
+ flag = "--#{key.to_s.dasherize}"
50
+ case value
51
+ when true then [ flag ]
52
+ when false then [ "--no-#{key.to_s.dasherize}" ]
53
+ when Array then [ flag, *value.map(&:to_s) ]
54
+ else [ flag, value.to_s ]
55
+ end
56
+ end
57
+ end
58
+
59
+ def check_syntax!(code)
60
+ RubyVM::InstructionSequence.compile(code)
61
+ rescue SyntaxError => e
62
+ raise Error, "generated code does not parse (check authorize/sort values): #{e.message.lines.first&.strip}"
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,122 @@
1
+ module Infotable
2
+ module Mcp
3
+ class ListModels < ::MCP::Tool
4
+ tool_name "list_models"
5
+ description "Models of this Rails app with column types, enums and associations. Call first, so column names are not guessed."
6
+ input_schema(properties: {})
7
+ annotations(read_only_hint: true)
8
+
9
+ def self.call(server_context: nil)
10
+ Mcp.text(Introspection.models)
11
+ end
12
+ end
13
+
14
+ class ListTables < ::MCP::Tool
15
+ tool_name "list_tables"
16
+ description "InfoTable tables that already exist in app/infotable, with their columns and settings. Use them as the house style."
17
+ input_schema(properties: {})
18
+ annotations(read_only_hint: true)
19
+
20
+ def self.call(server_context: nil)
21
+ Mcp.text(Introspection.tables)
22
+ end
23
+ end
24
+
25
+ class DescribeDsl < ::MCP::Tool
26
+ tool_name "describe_dsl"
27
+ description "The table DSL of the installed infotable version: column options, types, filters, formatters, settings. The full guide is the infotable://readme resource."
28
+ input_schema(properties: {})
29
+ annotations(read_only_hint: true)
30
+
31
+ def self.call(server_context: nil)
32
+ Mcp.text(Introspection.dsl)
33
+ end
34
+ end
35
+
36
+ class TableOptions < ::MCP::Tool
37
+ tool_name "table_options"
38
+ description "Settings of a new table for a model, as questions with defaults proposed from the model. " \
39
+ "Ask the user these (all at once, defaults pre-filled) before calling generate_table."
40
+ input_schema(properties: { model: { type: "string", description: "Model class name, e.g. Order or Admin::Invoice" } }, required: [ "model" ])
41
+ annotations(read_only_hint: true)
42
+
43
+ def self.call(model:, server_context: nil)
44
+ Mcp.text(Introspection.table_options(model) || "#{model} is not an ActiveRecord model; call list_models")
45
+ end
46
+ end
47
+
48
+ class GenerateTable < ::MCP::Tool
49
+ tool_name "generate_table"
50
+ description "Generates app/infotable/<models>_table.rb for a model with the infotable:table generator. " \
51
+ "Shows the code only; set write: true to save it (refuses to replace a file unless overwrite: true). " \
52
+ "Answers from table_options map to the parameters. Run validate_table afterwards."
53
+ input_schema(
54
+ properties: {
55
+ model: { type: "string" },
56
+ only: { type: "array", items: { type: "string" }, description: "Only these columns" },
57
+ skip: { type: "array", items: { type: "string" }, description: "Leave these columns out" },
58
+ per_page: { type: "integer" },
59
+ sort: { type: "string", description: "column:direction, e.g. created_at:desc" },
60
+ toolbar: { type: "string", enum: %w[header rail] },
61
+ row_click: { type: "boolean" },
62
+ infinite_scroll: { type: "boolean" },
63
+ countless: { type: "boolean" },
64
+ checkbox: { type: "boolean" },
65
+ authorize: { type: "string", description: "Ruby expression over `controller`, e.g. controller.current_user&.admin?" },
66
+ write: { type: "boolean", description: "Save the file (default: only show the code)" },
67
+ overwrite: { type: "boolean" }
68
+ },
69
+ required: [ "model" ]
70
+ )
71
+
72
+ def self.call(model:, write: false, overwrite: false, server_context: nil, **settings)
73
+ writer = TableWriter.new(model, settings)
74
+ result = write ? writer.write(overwrite: overwrite) : writer.render
75
+ Mcp.text(path: result.path, written: write, code: result.code)
76
+ rescue TableWriter::Error => e
77
+ Mcp.error(e.message)
78
+ end
79
+ end
80
+
81
+ class ValidateTable < ::MCP::Tool
82
+ tool_name "validate_table"
83
+ description "Loads a table (by name like posts_table, or from source code) and reports mistakes: missing columns, bad default sort, " \
84
+ "failing query. preview: N also returns the first N rows as the table would show them."
85
+ input_schema(
86
+ properties: {
87
+ name: { type: "string", description: "Table name, e.g. orders_table or admin_orders_table" },
88
+ source: { type: "string", description: "Table source code to check without saving it" },
89
+ preview: { type: "integer", description: "Rows to return (max 20)" }
90
+ }
91
+ )
92
+ annotations(read_only_hint: true)
93
+
94
+ def self.call(name: nil, source: nil, preview: 0, server_context: nil)
95
+ return Mcp.error("pass name or source") if name.blank? && source.blank?
96
+
97
+ Mcp.text(TableChecker.new(name: name, source: source, preview: preview).call)
98
+ rescue Infotable::TableResolver::NotFound, ScriptError, StandardError => e
99
+ Mcp.error("#{e.class}: #{e.message}")
100
+ end
101
+ end
102
+
103
+ class NewTablePrompt < ::MCP::Prompt
104
+ prompt_name "new_table"
105
+ description "Create an InfoTable table for a model, asking the user about its settings first"
106
+ arguments [ ::MCP::Prompt::Argument.new(name: "model", description: "Model class name", required: true) ]
107
+
108
+ def self.template(args, server_context: nil)
109
+ model = args["model"] || args[:model]
110
+ text = <<~TEXT
111
+ Create an InfoTable table for #{model}.
112
+ 1. Call list_tables to see how tables in this app look, and table_options for #{model}.
113
+ 2. Ask the user the questions of table_options in ONE message, with the proposed defaults filled in, and wait for the answers.
114
+ 3. Call generate_table with the answers (without write) and show the code.
115
+ 4. After the user agrees, call generate_table with write: true, then validate_table with preview: 5 and report the result.
116
+ Add a batch action, formatter or association column only when the user asks; describe_dsl lists what exists.
117
+ TEXT
118
+ ::MCP::Prompt::Result.new(messages: [ ::MCP::Prompt::Message.new(role: "user", content: ::MCP::Content::Text.new(text)) ])
119
+ end
120
+ end
121
+ end
122
+ end
@@ -0,0 +1,30 @@
1
+ begin
2
+ require "mcp"
3
+ rescue LoadError
4
+ abort "infotable-mcp needs the mcp gem: add `gem \"mcp\", group: :development` to your Gemfile and run bundle install"
5
+ end
6
+
7
+ require "json"
8
+ require "rails/generators"
9
+ require "tmpdir"
10
+
11
+ module Infotable
12
+ # MCP server for creating tables with an AI assistant. See exe/infotable-mcp.
13
+ module Mcp
14
+ autoload :Introspection, "infotable/mcp/introspection"
15
+ autoload :Server, "infotable/mcp/server"
16
+ autoload :TableWriter, "infotable/mcp/table_writer"
17
+ autoload :TableChecker, "infotable/mcp/table_checker"
18
+
19
+ def self.text(payload)
20
+ text = payload.is_a?(String) ? payload : JSON.pretty_generate(payload)
21
+ ::MCP::Tool::Response.new([ { type: "text", text: text } ])
22
+ end
23
+
24
+ def self.error(message)
25
+ ::MCP::Tool::Response.new([ { type: "text", text: message } ], error: true)
26
+ end
27
+ end
28
+ end
29
+
30
+ require "infotable/mcp/tools"
@@ -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
 
@@ -1,3 +1,3 @@
1
1
  module Infotable
2
- VERSION = "1.4.1"
2
+ VERSION = "1.6.0"
3
3
  end
data/lib/infotable.rb CHANGED
@@ -1,3 +1,4 @@
1
+ require "rails-i18n"
1
2
  require "infotable/version"
2
3
  require "infotable/configuration"
3
4
  require "infotable/engine"
@@ -24,6 +25,27 @@ module Infotable
24
25
  autoload :LinkFormatter, "infotable/formatters/link_formatter"
25
26
  autoload :AvatarFormatter, "infotable/formatters/avatar_formatter"
26
27
  autoload :PercentFormatter, "infotable/formatters/percent_formatter"
28
+ autoload :NumberFormatter, "infotable/formatters/number_formatter"
29
+ autoload :TagsFormatter, "infotable/formatters/tags_formatter"
30
+ autoload :TruncateFormatter, "infotable/formatters/truncate_formatter"
31
+ autoload :ImageFormatter, "infotable/formatters/image_formatter"
32
+ autoload :FilesizeFormatter, "infotable/formatters/filesize_formatter"
33
+ autoload :DurationFormatter, "infotable/formatters/duration_formatter"
34
+
35
+ # `formatter: :name` => class. Anything else is looked up as a custom "<Name>Formatter" constant.
36
+ def self.built_in
37
+ @built_in ||= {
38
+ date: DateFormatter, datetime: DateFormatter,
39
+ currency: CurrencyFormatter,
40
+ boolean: BooleanFormatter,
41
+ badge: BadgeFormatter, status: BadgeFormatter,
42
+ action: ActionFormatter, actions: ActionFormatter,
43
+ link: LinkFormatter, avatar: AvatarFormatter, percent: PercentFormatter,
44
+ number: NumberFormatter, tags: TagsFormatter, list: TagsFormatter,
45
+ truncate: TruncateFormatter, image: ImageFormatter,
46
+ filesize: FilesizeFormatter, duration: DurationFormatter
47
+ }.freeze
48
+ end
27
49
  end
28
50
 
29
51
  # Returns the root path of the InfoTable gem
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: infotable
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.4.1
4
+ version: 1.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Jakub Cieślar
8
8
  autorequire:
9
- bindir: bin
9
+ bindir: exe
10
10
  cert_chain: []
11
- date: 2026-10-25 00:00:00.000000000 Z
11
+ date: 2026-10-26 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails
@@ -24,6 +24,20 @@ dependencies:
24
24
  - - ">="
25
25
  - !ruby/object:Gem::Version
26
26
  version: '8.0'
27
+ - !ruby/object:Gem::Dependency
28
+ name: rails-i18n
29
+ requirement: !ruby/object:Gem::Requirement
30
+ requirements:
31
+ - - ">="
32
+ - !ruby/object:Gem::Version
33
+ version: '8.0'
34
+ type: :runtime
35
+ prerelease: false
36
+ version_requirements: !ruby/object:Gem::Requirement
37
+ requirements:
38
+ - - ">="
39
+ - !ruby/object:Gem::Version
40
+ version: '8.0'
27
41
  - !ruby/object:Gem::Dependency
28
42
  name: csv
29
43
  requirement: !ruby/object:Gem::Requirement
@@ -122,12 +136,27 @@ dependencies:
122
136
  - - ">="
123
137
  - !ruby/object:Gem::Version
124
138
  version: '0'
139
+ - !ruby/object:Gem::Dependency
140
+ name: mcp
141
+ requirement: !ruby/object:Gem::Requirement
142
+ requirements:
143
+ - - ">="
144
+ - !ruby/object:Gem::Version
145
+ version: '0'
146
+ type: :development
147
+ prerelease: false
148
+ version_requirements: !ruby/object:Gem::Requirement
149
+ requirements:
150
+ - - ">="
151
+ - !ruby/object:Gem::Version
152
+ version: '0'
125
153
  description: InfoTable provides server-side datatable functionality with filtering,
126
154
  sorting, searching, and pagination. Built with Hotwire, Stimulus, and Tailwind CSS
127
155
  for a modern, no-reload user experience.
128
156
  email:
129
157
  - jakub@infovation.pl
130
- executables: []
158
+ executables:
159
+ - infotable-mcp
131
160
  extensions: []
132
161
  extra_rdoc_files: []
133
162
  files:
@@ -137,6 +166,7 @@ files:
137
166
  - app/assets/javascripts/infotable/controllers/drawer_controller.js
138
167
  - app/assets/javascripts/infotable/controllers/table_controller.js
139
168
  - app/assets/javascripts/infotable/index.js
169
+ - app/assets/javascripts/infotable/vendor/sortable.esm.js
140
170
  - app/assets/tailwind/infotable/engine.css
141
171
  - app/controllers/infotable/application_controller.rb
142
172
  - app/controllers/infotable/tables_controller.rb
@@ -147,6 +177,7 @@ files:
147
177
  - app/models/infotable/application_record.rb
148
178
  - app/views/infotable/tables/_active_filters.html.erb
149
179
  - app/views/infotable/tables/_column_visibility.html.erb
180
+ - app/views/infotable/tables/_end_indicator.html.erb
150
181
  - app/views/infotable/tables/_filter_drawer.html.erb
151
182
  - app/views/infotable/tables/_next_page_frame.html.erb
152
183
  - app/views/infotable/tables/_pagination.html.erb
@@ -163,9 +194,15 @@ files:
163
194
  - app/views/infotable/tables/show.html.erb
164
195
  - app/views/layouts/infotable/application.html.erb
165
196
  - config/importmap.rb
197
+ - config/locales/de.yml
198
+ - config/locales/el.yml
166
199
  - config/locales/en.yml
200
+ - config/locales/es.yml
201
+ - config/locales/fr.yml
202
+ - config/locales/it.yml
167
203
  - config/locales/pl.yml
168
204
  - config/routes.rb
205
+ - exe/infotable-mcp
169
206
  - lib/generators/infotable/install/install_generator.rb
170
207
  - lib/generators/infotable/install/templates/initializer.rb
171
208
  - lib/generators/infotable/install/templates/tailwind.config.js
@@ -184,8 +221,20 @@ files:
184
221
  - lib/infotable/formatters/boolean_formatter.rb
185
222
  - lib/infotable/formatters/currency_formatter.rb
186
223
  - lib/infotable/formatters/date_formatter.rb
224
+ - lib/infotable/formatters/duration_formatter.rb
225
+ - lib/infotable/formatters/filesize_formatter.rb
226
+ - lib/infotable/formatters/image_formatter.rb
187
227
  - lib/infotable/formatters/link_formatter.rb
228
+ - lib/infotable/formatters/number_formatter.rb
188
229
  - lib/infotable/formatters/percent_formatter.rb
230
+ - lib/infotable/formatters/tags_formatter.rb
231
+ - lib/infotable/formatters/truncate_formatter.rb
232
+ - lib/infotable/mcp.rb
233
+ - lib/infotable/mcp/introspection.rb
234
+ - lib/infotable/mcp/server.rb
235
+ - lib/infotable/mcp/table_checker.rb
236
+ - lib/infotable/mcp/table_writer.rb
237
+ - lib/infotable/mcp/tools.rb
189
238
  - lib/infotable/search_processor.rb
190
239
  - lib/infotable/sql.rb
191
240
  - lib/infotable/table_resolver.rb