infotable 1.5.0 → 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 (41) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +144 -10
  3. data/app/assets/javascripts/infotable/controllers/drawer_controller.js +19 -18
  4. data/app/assets/javascripts/infotable/controllers/table_controller.js +42 -23
  5. data/app/controllers/infotable/tables_controller.rb +1 -1
  6. data/app/helpers/infotable/tables_helper.rb +10 -25
  7. data/app/views/infotable/tables/_filter_drawer.html.erb +6 -6
  8. data/app/views/infotable/tables/_pagination.html.erb +4 -7
  9. data/app/views/infotable/tables/_table.html.erb +1 -1
  10. data/app/views/infotable/tables/filters/_boolean.html.erb +1 -1
  11. data/app/views/infotable/tables/filters/_range.html.erb +1 -1
  12. data/app/views/infotable/tables/filters/_select.html.erb +2 -2
  13. data/app/views/infotable/tables/filters/_text.html.erb +2 -2
  14. data/config/locales/de.yml +126 -0
  15. data/config/locales/el.yml +126 -0
  16. data/config/locales/en.yml +18 -4
  17. data/config/locales/es.yml +126 -0
  18. data/config/locales/fr.yml +126 -0
  19. data/config/locales/it.yml +126 -0
  20. data/config/locales/pl.yml +18 -4
  21. data/exe/infotable-mcp +13 -0
  22. data/lib/generators/infotable/table/table_generator.rb +23 -1
  23. data/lib/generators/infotable/table/templates/table.rb.tt +8 -1
  24. data/lib/infotable/base.rb +5 -4
  25. data/lib/infotable/formatters/currency_formatter.rb +4 -12
  26. data/lib/infotable/formatters/date_formatter.rb +3 -9
  27. data/lib/infotable/formatters/duration_formatter.rb +51 -0
  28. data/lib/infotable/formatters/filesize_formatter.rb +14 -0
  29. data/lib/infotable/formatters/image_formatter.rb +31 -0
  30. data/lib/infotable/formatters/number_formatter.rb +46 -0
  31. data/lib/infotable/formatters/tags_formatter.rb +44 -0
  32. data/lib/infotable/formatters/truncate_formatter.rb +19 -0
  33. data/lib/infotable/mcp/introspection.rb +94 -0
  34. data/lib/infotable/mcp/server.rb +26 -0
  35. data/lib/infotable/mcp/table_checker.rb +80 -0
  36. data/lib/infotable/mcp/table_writer.rb +66 -0
  37. data/lib/infotable/mcp/tools.rb +122 -0
  38. data/lib/infotable/mcp.rb +30 -0
  39. data/lib/infotable/version.rb +1 -1
  40. data/lib/infotable.rb +22 -0
  41. metadata +51 -4
@@ -0,0 +1,80 @@
1
+ module Infotable
2
+ module Mcp
3
+ # Loads a table and looks for mistakes before the app does: columns the model lacks,
4
+ # a default sort on a missing column, a query that raises. Optionally returns a few rows.
5
+ class TableChecker
6
+ def initialize(name: nil, source: nil, preview: 0)
7
+ @name = name
8
+ @source = source
9
+ @preview = preview.to_i.clamp(0, 20)
10
+ end
11
+
12
+ def call
13
+ table = load_table
14
+ problems = []
15
+ model = nil
16
+
17
+ begin
18
+ model = table.base_query.model
19
+ rescue StandardError => e
20
+ return { ok: false, problems: [ "query raises #{e.class}: #{e.message}" ] }
21
+ end
22
+
23
+ problems.concat(column_problems(table, model))
24
+ problems.concat(sort_problems(table))
25
+ problems << "no authorize block: anyone who can reach the engine routes can read this table" if table.table_authorize.nil?
26
+
27
+ rows = preview_rows(table, problems)
28
+ { ok: problems.none? { !_1.start_with?("no authorize") }, table: table.name, model: model.name, problems: problems, preview: rows }.compact
29
+ end
30
+
31
+ private
32
+
33
+ def load_table
34
+ if @source
35
+ mod = Module.new
36
+ mod.module_eval(@source.sub(/class \S+ </, "class CheckedTable <"), "infotable_mcp_check.rb")
37
+ mod.const_get(:CheckedTable)
38
+ else
39
+ reload_app
40
+ Infotable::TableResolver.resolve(@name)
41
+ end
42
+ end
43
+
44
+ # A table written a moment ago is not known to the autoloader yet
45
+ def reload_app
46
+ Rails.application.reloader.reload! if Rails.application.config.enable_reloading
47
+ rescue StandardError
48
+ nil
49
+ end
50
+
51
+ def column_problems(table, model)
52
+ table.columns.filter_map do |col|
53
+ next if col[:value] || col[:virtual] || col[:sql] || %i[action actions].include?(col[:type])
54
+ next if model.column_names.include?(col[:name].to_s) || model.method_defined?(col[:name])
55
+
56
+ "column :#{col[:name]} is not an attribute of #{model.name}: add value:, sql: or virtual:"
57
+ end
58
+ end
59
+
60
+ def sort_problems(table)
61
+ column = table.sort_settings[:column]
62
+ return [] if column.nil? || table.columns.any? { _1[:name].to_s == column.to_s }
63
+
64
+ [ "default_sort :#{column} is not a defined column" ]
65
+ end
66
+
67
+ def preview_rows(table, problems)
68
+ return nil if @preview.zero?
69
+
70
+ columns = table.visible_columns.reject { %i[action actions].include?(_1[:type]) }
71
+ table.base_query.limit(@preview).map do |record|
72
+ columns.to_h { |col| [ col[:name], table.cell_value(record, col).to_s ] }
73
+ end
74
+ rescue StandardError => e
75
+ problems << "query raises #{e.class}: #{e.message}"
76
+ nil
77
+ end
78
+ end
79
+ end
80
+ end
@@ -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"
@@ -1,3 +1,3 @@
1
1
  module Infotable
2
- VERSION = "1.5.0"
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.5.0
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:
@@ -165,9 +194,15 @@ files:
165
194
  - app/views/infotable/tables/show.html.erb
166
195
  - app/views/layouts/infotable/application.html.erb
167
196
  - config/importmap.rb
197
+ - config/locales/de.yml
198
+ - config/locales/el.yml
168
199
  - config/locales/en.yml
200
+ - config/locales/es.yml
201
+ - config/locales/fr.yml
202
+ - config/locales/it.yml
169
203
  - config/locales/pl.yml
170
204
  - config/routes.rb
205
+ - exe/infotable-mcp
171
206
  - lib/generators/infotable/install/install_generator.rb
172
207
  - lib/generators/infotable/install/templates/initializer.rb
173
208
  - lib/generators/infotable/install/templates/tailwind.config.js
@@ -186,8 +221,20 @@ files:
186
221
  - lib/infotable/formatters/boolean_formatter.rb
187
222
  - lib/infotable/formatters/currency_formatter.rb
188
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
189
227
  - lib/infotable/formatters/link_formatter.rb
228
+ - lib/infotable/formatters/number_formatter.rb
190
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
191
238
  - lib/infotable/search_processor.rb
192
239
  - lib/infotable/sql.rb
193
240
  - lib/infotable/table_resolver.rb