inline_forms 8.1.0 → 8.1.2

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b3b934b94b7f4b5298dea2e2a51b8c97c37c06969a3484d528e336edf17ddfde
4
- data.tar.gz: fedfafb7fba445712299b4c240fc905418c665d8f29e139c17cb9abc4138fe36
3
+ metadata.gz: 1cb2062c3fc919203715606f7a499b51f35cac4882d681098499e48ee7b6f06f
4
+ data.tar.gz: 8f08d99f8c9b161c398919747e8fe457e0e6c91370d721eb8e300e9fd89d934a
5
5
  SHA512:
6
- metadata.gz: b9d1abfecdf6fa9c75c4d79967e8604e0bb408a40a08475fb7106e12b0b301a437e8192f836a39b9b5c75261ac97d5dfade6f2ba5ecea9005015814bd257028f
7
- data.tar.gz: 5f62f997de89dd93334a6324d2d02fe3bd283036d3f68ee39947cd400ab57f6d3a2d959745498d2b7f5e5c567889cd02e258ed7c15fef3cf3f051f3a21ee3d60
6
+ metadata.gz: f72943d7626636b4657c09b79aa47feff78fd533fe179bcc5200322ba49b48dceee70b4cff15362b715af47d34e40c0ff2184e879c1e0cee715698bd236cd223
7
+ data.tar.gz: e7aea2a16935eb0c0fb7bae12a2c2bd83ed1ce8324d29d750523c624e72b15233574d18f520bb64b4c8495a054a7d841958451c83a81608e3f6f62cc6b55346b
data/CHANGELOG.md CHANGED
@@ -4,6 +4,37 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [8.1.2] - 2026-05-26
8
+
9
+ ### Changed
10
+
11
+ - **Rails 8.1:** engine and installer pin **`rails >= 8.1, < 8.2`** (resolved **8.1.3**) and **`rails-i18n >= 8.1, < 9.0`** (resolved **8.1.0**). Generated apps' `config/application.rb` is normalised to **`config.load_defaults 8.1`** and the installer prefers **`rails _8.1.x_`** for `rails new`. All generator and installer migrations now emit **`ActiveRecord::Migration[8.1]`** (`lib/generators/templates/migration.erb`, `lib/generators/templates/add_columns_migration.erb`, `DeviseCreateUsers`, `InlineFormsCreateJoinTableUserRole`, `AddOwnerToApartments`, `SeedExampleApartmentsAndOwners`).
12
+ - **`validation_hints` constraint:** widened to **`>= 8.1.2, < 9.0`** so Bundler picks up the matching companion release that targets `activerecord >= 8.1`.
13
+ - **README.rdoc:** requirements table and `rails-i18n` narrative updated to **Rails 8.1.x** / `~> 8.1` / `config.load_defaults 8.1`.
14
+ - **`InlineForms::VERSION`** and **`InlineFormsInstaller::VERSION`** → **8.1.2** (lockstep with **validation_hints 8.1.2**).
15
+
16
+ ### Notes
17
+
18
+ - **Example app gate (recorded):** `inline_forms create MyApp -d sqlite --example` against the freshly built **8.1.2** gem trio on `rails 8.1.3`: install completes in ~77s, `bundle check: ok`, **88 runs, 502 assertions, 0 failures, 0 errors, 0 skips**. A subsequent `bundle exec rails test` in the generated app reproduces the same result in ~1.9s.
19
+ - **PaperTrail 16.0.0 / ActiveRecord 8.1 warning:** PT emits a compatibility warning during boot (it pins `< 8.1` internally) but does not raise; all PaperTrail-backed integration and model tests pass on AR 8.1.3. Revisit pin when paper_trail ships an 8.1-compatible release; no behaviour change needed in inline_forms for now.
20
+ - **Frozen-string warnings:** Ruby 4.0's `--debug-frozen-string-literal` surfaces literal-string warnings from `tabs_on_rails 3.0.0` and one inline_forms helper (`check_list_helper.rb:13`); these are non-fatal under current Ruby and tracked separately.
21
+
22
+ ## [8.1.1] - 2026-05-26
23
+
24
+ ### Added
25
+
26
+ - **`rails g inline_forms_addto <Model> attr:type [...]`:** new generator that adds fields to an *existing* inline_forms model. Emits an additive migration (`add_column` for scalars, `add_reference :table, :name, foreign_key: true` for `:belongs_to`/`:references`/`:dropdown`, single `:string` column for `:image_field`/`:audio_field`; no column for `:rich_text`/`has_many`/`has_one`/`habtm`) and edits the model file: injects `belongs_to` / `has_many` / `has_one` / `has_and_belongs_to_many` / `has_rich_text` / `mount_uploader` lines (idempotent), and inserts new rows into the model's `inline_forms_attribute_list` (handles both the generator-shaped and the installer's hand-written `User`/`Member` block). Does NOT touch routes, controller, or `MODEL_TABS` — those are already wired for the existing model. Use case: extending the narrow `User` (or `--user-model Member`) shipped by the installer with app-specific fields like `occupation`, `birthdate`, `organization:dropdown`, `bio:rich_text` without hand-writing migrations or attribute-list edits. Plan: [`stuff/inline_forms_addto.md`](stuff/inline_forms_addto.md).
27
+ - **`--allow-unknown` parity** with `inline_forms`: unknown field types raise `Thor::Error` by default; pass `--allow-unknown` to keep the legacy commented output (`# add_column …`, `# [ :name , "name", :unknown ]`).
28
+ - **`--replace`** flag: opt-in rewrite of existing `_presentation` / `_list_order` (and the legacy `_order`) / `_list_search` scopes/methods. Without `--replace` these special names are skipped with `say_status :skipped` so re-runs don't silently mutate the model's ordering/search policy.
29
+
30
+ ### Changed
31
+
32
+ - **Refactor — `Rails::Generators::GeneratedAttribute` overrides moved to `lib/generators/inline_forms_attribute_overrides.rb`** and shared by both `InlineFormsGenerator` (`rails g inline_forms`) and the new `InlineFormsAddtoGenerator` (`rails g inline_forms_addto`). Pure refactor: same `column_type` / `attribute_type` / `relation?` / `migration?` / `attribute?` / `SPECIAL_GENERATOR_NAMES` semantics, now qualified with `InlineForms::` constants so the file works outside the `module InlineForms` lexical scope.
33
+
34
+ ### Notes
35
+
36
+ - The new generator class is top-level (`InlineFormsAddtoGenerator`, not `InlineForms::InlineFormsAddtoGenerator`) so `rails g inline_forms_addto …` resolves directly. Rails' generator lookup only collapses `name:name` namespaces (the `inline_forms:inline_forms` → `inline_forms` rule that `InlineFormsGenerator` relies on). A `module InlineForms; InlineFormsAddtoGenerator = ::InlineFormsAddtoGenerator; end` alias is provided for any programmatic invokers that reach for the namespaced constant.
37
+
7
38
  ## [8.1.0] - 2026-05-25
8
39
 
9
40
  ### Added
data/README.rdoc CHANGED
@@ -5,7 +5,7 @@ Inline Forms is almost a complete admin application. You can try it out easily.
5
5
  = Requirements
6
6
 
7
7
  * Ruby **>= 4.0** (generated apps pin **ruby-4.0.4**)
8
- * Rails **8.0.x** (+rails ~> 8.0+, +config.load_defaults 8.0+)
8
+ * Rails **8.1.x** (+rails ~> 8.1+, +config.load_defaults 8.1+)
9
9
  * **validation_hints** **~> 8** (companion gem; same version line as +inline_forms+ / +inline_forms_installer+)
10
10
 
11
11
  = Usage
@@ -252,7 +252,7 @@ In every case the Turbo wiring is the same: +link_options: { data: { turbo_frame
252
252
 
253
253
  == Generated application +rails-i18n+
254
254
 
255
- New apps get +rails-i18n+ from RubyGems (+ '~> 8.0'+), not from the +svenfuchs/rails-i18n+ Git repository. The installer pins +rails ~> 8.0+ with +config.load_defaults 8.0+; the published +rails-i18n+ 8.x line matches that stack.
255
+ New apps get +rails-i18n+ from RubyGems (+ '~> 8.1'+), not from the +svenfuchs/rails-i18n+ Git repository. The installer pins +rails ~> 8.1+ with +config.load_defaults 8.1+; the published +rails-i18n+ 8.x line matches that stack.
256
256
 
257
257
  == File uploads (CarrierWave)
258
258
 
data/inline_forms.gemspec CHANGED
@@ -11,7 +11,7 @@ Gem::Specification.new do |s|
11
11
  s.email = ["ace@suares.com"]
12
12
  s.homepage = %q{http://github.com/acesuares/inline_forms}
13
13
  s.summary = %q{Inline editing of forms for Rails 8.}
14
- s.description = %q{Inline Forms eases setup of admin-style forms with inline editing. Field lists are declared on the model. Requires Rails 8.0.x, Ruby >= 4.0, and validation_hints ~> 8.}
14
+ s.description = %q{Inline Forms eases setup of admin-style forms with inline editing. Field lists are declared on the model. Requires Rails 8.1.x, Ruby >= 4.0, and validation_hints ~> 8.}
15
15
  s.licenses = ["MIT"]
16
16
  s.required_ruby_version = ">= 4.0.0"
17
17
 
@@ -19,9 +19,9 @@ Gem::Specification.new do |s|
19
19
  s.test_files = s.files.grep(%r{^(test|spec|features)/})
20
20
  s.require_paths = ["lib"]
21
21
 
22
- s.add_dependency("validation_hints", ">= 8.1.0", "< 9.0")
23
- s.add_dependency("rails", ">= 8.0", "< 8.1")
24
- s.add_dependency("rails-i18n", ">= 8.0", "< 9.0")
22
+ s.add_dependency("validation_hints", ">= 8.1.2", "< 9.0")
23
+ s.add_dependency("rails", ">= 8.1", "< 8.2")
24
+ s.add_dependency("rails-i18n", ">= 8.1", "< 9.0")
25
25
 
26
26
  s.add_development_dependency("minitest", "~> 5.0")
27
27
  end
@@ -0,0 +1,291 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "inline_forms"
3
+ require_relative "inline_forms_attribute_overrides"
4
+
5
+ # == Usage
6
+ #
7
+ # rails g inline_forms_addto Model attribute:type [attribute:type ...] [options]
8
+ #
9
+ # Adds fields to an existing inline_forms model. Emits an additive
10
+ # migration (`add_column` / `add_reference` / single :string column for
11
+ # `:image_field`/`:audio_field`) and edits the model file:
12
+ #
13
+ # * adds `belongs_to` / `has_many` / `has_one` / `has_and_belongs_to_many` /
14
+ # `has_rich_text` / `mount_uploader` lines (idempotent)
15
+ # * inserts new rows into the model's `inline_forms_attribute_list`
16
+ #
17
+ # Does NOT touch routes, the controller, or `MODEL_TABS` (those are
18
+ # already wired for the existing model). Use `rails g inline_forms <Model>`
19
+ # to create new models.
20
+ #
21
+ # == Special generator names
22
+ #
23
+ # `_no_model`, `_no_migration`, `_id`, `_enabled` are install-time only and
24
+ # are rejected. `_presentation`, `_list_order`, `_list_search` are
25
+ # accepted but only acted on when `--replace` is passed; otherwise the
26
+ # generator skips them with a `say_status :skipped` notice.
27
+ #
28
+ # Top-level (no `InlineForms` module wrapper) so Rails resolves
29
+ # `rails g inline_forms_addto Model …` to this class without the
30
+ # `inline_forms:inline_forms_addto` double-segment trick that Rails only
31
+ # applies for matching `name:name` pairs (`inline_forms:inline_forms`).
32
+ class InlineFormsAddtoGenerator < Rails::Generators::NamedBase
33
+ INSTALL_TIME_ONLY_NAMES = %w[_no_model _no_migration _id _enabled].freeze
34
+ REPLACE_ONLY_NAMES = %w[_presentation _list_order _list_search _order].freeze
35
+
36
+ argument :attributes, type: :array, banner: "[name:form_element]..."
37
+ class_option :allow_unknown, type: :boolean, default: false,
38
+ desc: "Allow unknown field types (legacy behavior: comment generated lines instead of failing)."
39
+ class_option :replace, type: :boolean, default: false,
40
+ desc: "Replace existing _presentation/_list_order/_list_search instead of skipping."
41
+
42
+ source_root File.expand_path("templates", __dir__)
43
+
44
+ def validate!
45
+ unless File.exist?(File.join(destination_root, model_file_path))
46
+ raise Thor::Error,
47
+ "Model #{model_file_path} not found. " \
48
+ "Use `rails g inline_forms #{name} ...` for new models."
49
+ end
50
+ end
51
+
52
+ def set_some_flags
53
+ @unknown_attributes = []
54
+ @forbidden_names = []
55
+ @skipped_replace_names = []
56
+ attributes.each do |attribute|
57
+ if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
58
+ @forbidden_names << attribute.name
59
+ next
60
+ end
61
+ if REPLACE_ONLY_NAMES.include?(attribute.name) && !options[:replace]
62
+ @skipped_replace_names << "#{attribute.name}:#{attribute.type}"
63
+ next
64
+ end
65
+ if !attribute.name.start_with?("_") && (
66
+ (attribute.attribute? && attribute.attribute_type == :unknown) ||
67
+ (attribute.migration? && attribute.column_type == :unknown)
68
+ )
69
+ @unknown_attributes << "#{attribute.name}:#{attribute.type}"
70
+ end
71
+ end
72
+
73
+ unless @forbidden_names.empty?
74
+ raise Thor::Error,
75
+ "Names #{@forbidden_names.uniq.join(', ')} are install-time only " \
76
+ "and not supported by inline_forms_addto."
77
+ end
78
+
79
+ allow_unknown = options[:allow_unknown].to_s == "true"
80
+ if !allow_unknown && !@unknown_attributes.empty?
81
+ raise Thor::Error,
82
+ "Unknown field type(s): #{@unknown_attributes.uniq.join(', ')}. " \
83
+ "Add a valid form element type or pass --allow-unknown to keep legacy commented output."
84
+ end
85
+
86
+ @skipped_replace_names.each do |label|
87
+ say_status :skipped, "#{label} (pass --replace to rewrite the existing scope/method)", :yellow
88
+ end
89
+ end
90
+
91
+ def generate_migration
92
+ @migration_lines = String.new
93
+ attributes.each do |attribute|
94
+ next if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
95
+ next if REPLACE_ONLY_NAMES.include?(attribute.name)
96
+ next unless attribute.migration?
97
+
98
+ if attribute.column_type == :belongs_to
99
+ @migration_lines << " add_reference :#{table_name}, :#{attribute.name}, foreign_key: true\n"
100
+ else
101
+ commenter = attribute.attribute_type == :unknown ? "#" : " "
102
+ @migration_lines << "#{commenter} add_column :#{table_name}, :#{attribute.name}, :#{attribute.column_type}\n"
103
+ end
104
+ end
105
+
106
+ return if @migration_lines.empty?
107
+
108
+ template "add_columns_migration.erb",
109
+ "db/migrate/#{time_stamp}_#{migration_file_basename}.rb"
110
+ end
111
+
112
+ def update_model
113
+ attributes.each do |attribute|
114
+ next if INSTALL_TIME_ONLY_NAMES.include?(attribute.name)
115
+ next if REPLACE_ONLY_NAMES.include?(attribute.name) && !options[:replace]
116
+
117
+ if REPLACE_ONLY_NAMES.include?(attribute.name) && options[:replace]
118
+ replace_special!(attribute)
119
+ next
120
+ end
121
+
122
+ case attribute.column_type
123
+ when :belongs_to
124
+ inject_class_line!(" belongs_to :#{attribute.name}\n")
125
+ end
126
+
127
+ case attribute.type
128
+ when :image_field, :audio_field
129
+ uploader_class = "#{attribute.name}_uploader".camelcase
130
+ inject_class_line!(" mount_uploader :#{attribute.name}, #{uploader_class}\n")
131
+ when :has_many
132
+ inject_class_line!(" has_many :#{attribute.name}\n")
133
+ when :has_many_destroy
134
+ inject_class_line!(" has_many :#{attribute.name}, :dependent => :destroy\n")
135
+ when :has_one
136
+ inject_class_line!(" has_one :#{attribute.name}\n")
137
+ when :habtm, :has_and_belongs_to_many, :check_list
138
+ inject_class_line!(" has_and_belongs_to_many :#{attribute.name}\n")
139
+ when :rich_text
140
+ inject_class_line!(" has_rich_text :#{attribute.name}\n")
141
+ end
142
+
143
+ next unless attribute.attribute?
144
+
145
+ add_attribute_list_row!(attribute)
146
+ end
147
+ end
148
+
149
+ private
150
+
151
+ def model_file_name
152
+ name.underscore
153
+ end
154
+
155
+ def model_file_path
156
+ "app/models/#{model_file_name}.rb"
157
+ end
158
+
159
+ def model_class_name
160
+ name.camelize
161
+ end
162
+
163
+ def table_name
164
+ name.pluralize.underscore
165
+ end
166
+
167
+ def migration_class_name
168
+ "InlineFormsAddTo#{table_name.camelize}#{migration_class_suffix}"
169
+ end
170
+
171
+ # Suffix keeps re-runs from colliding (Rails would reject duplicate
172
+ # migration class names). Built from the column names being added.
173
+ def migration_class_suffix
174
+ cols = attributes.reject { |a| INSTALL_TIME_ONLY_NAMES.include?(a.name) || REPLACE_ONLY_NAMES.include?(a.name) }
175
+ .map { |a| a.name.camelize }
176
+ cols.empty? ? "Fields" : cols.first(3).join
177
+ end
178
+
179
+ def migration_file_basename
180
+ cols = attributes.reject { |a| INSTALL_TIME_ONLY_NAMES.include?(a.name) || REPLACE_ONLY_NAMES.include?(a.name) }
181
+ .map(&:name)
182
+ label = cols.first(3).join("_")
183
+ label = "fields" if label.empty?
184
+ "inline_forms_add_to_#{table_name}_#{label}"
185
+ end
186
+
187
+ def time_stamp
188
+ # Mirrors InlineFormsGenerator#time_stamp.
189
+ Time.now.utc.strftime("%Y%m%d%H%M%S")
190
+ end
191
+
192
+ # Idempotent inject: skip if `line` (sans trailing newline) already
193
+ # appears inside the model file. Uses `inject_into_class` so the
194
+ # injection lands at the top of the class body (after `class Foo <
195
+ # ApplicationRecord`).
196
+ def inject_class_line!(line)
197
+ content = File.read(File.join(destination_root, model_file_path))
198
+ probe = line.rstrip
199
+ if content.include?(probe)
200
+ say_status :identical, "#{model_file_path}: #{probe.strip}", :blue
201
+ return
202
+ end
203
+ inject_into_class model_file_path, model_class_name, line
204
+ end
205
+
206
+ # Inserts a new row into the existing `inline_forms_attribute_list`
207
+ # method. Falls back to appending a fresh method when the model has
208
+ # only `attr_writer :inline_forms_attribute_list` (inherited from
209
+ # `ApplicationRecord`) and no body.
210
+ def add_attribute_list_row!(attribute)
211
+ content = File.read(File.join(destination_root, model_file_path))
212
+ row = " [ :#{attribute.name} , \"#{attribute.name}\", :#{attribute.attribute_type} ],\n"
213
+
214
+ if content.match?(/@inline_forms_attribute_list \|\|=\s*\[/)
215
+ if content.match?(/\[\s*:#{Regexp.escape(attribute.name)}\s*,/)
216
+ say_status :identical, "#{model_file_path}: row :#{attribute.name}", :blue
217
+ return
218
+ end
219
+ # Insert before the closing `]` of the array, keeping appended order.
220
+ # Match against the file's full text and write it back so we work
221
+ # around Thor's `gsub_file` block-arg quirk (the block sees only the
222
+ # full match, no $1/$2). MatchData captures are explicit here.
223
+ m = content.match(/(@inline_forms_attribute_list \|\|=\s*\[(?:.|\n)*?)(\n\s*\]\s*\n\s*end)/)
224
+ if m
225
+ replacement = m[1] + "\n" + row.chomp + m[2]
226
+ new_content = content.sub(m[0], replacement)
227
+ File.write(File.join(destination_root, model_file_path), new_content)
228
+ say_status :insert, "#{model_file_path}: row :#{attribute.name}", :green
229
+ else
230
+ say_status :warn, "#{model_file_path}: could not locate end of @inline_forms_attribute_list, skipping :#{attribute.name}", :yellow
231
+ end
232
+ else
233
+ say_status :warn, "#{model_file_path}: no inline_forms_attribute_list found, appending a fresh method", :yellow
234
+ body = <<~RUBY.gsub(/^/, " ")
235
+
236
+ def inline_forms_attribute_list
237
+ @inline_forms_attribute_list ||= [
238
+ [ :#{attribute.name} , "#{attribute.name}", :#{attribute.attribute_type} ],
239
+ ]
240
+ end
241
+ RUBY
242
+ inject_into_class model_file_path, model_class_name, body
243
+ end
244
+ end
245
+
246
+ def replace_special!(attribute)
247
+ case attribute.name
248
+ when "_presentation"
249
+ new_method = " def _presentation\n \"#{attribute.type}\"\n end\n"
250
+ if File.read(File.join(destination_root, model_file_path)).match?(/def _presentation\b/)
251
+ gsub_file model_file_path,
252
+ / def _presentation\b.*?\n end\n/m,
253
+ new_method
254
+ else
255
+ inject_into_class model_file_path, model_class_name, "\n#{new_method}"
256
+ end
257
+ when "_list_order", "_order"
258
+ col = attribute.type
259
+ new_scope = " scope :inline_forms_list, -> { order(:#{col}, :id) }\n"
260
+ new_spaceship = " def <=>(other)\n self.#{col} <=> other.#{col}\n end\n"
261
+ path = File.join(destination_root, model_file_path)
262
+ content = File.read(path)
263
+ if content.match?(/scope :inline_forms_list\b/)
264
+ gsub_file model_file_path, / scope :inline_forms_list, ->.*\n/, new_scope
265
+ else
266
+ inject_into_class model_file_path, model_class_name, new_scope
267
+ end
268
+ if content.match?(/def <=>\(other\)/)
269
+ gsub_file model_file_path, / def <=>\(other\).*?\n end\n/m, new_spaceship
270
+ else
271
+ inject_into_class model_file_path, model_class_name, "\n#{new_spaceship}"
272
+ end
273
+ when "_list_search"
274
+ col = attribute.type
275
+ new_scope = " scope :inline_forms_search, ->(q) { where(\"#{col} LIKE ?\", \"%\#{q}%\") }\n"
276
+ if File.read(File.join(destination_root, model_file_path)).match?(/scope :inline_forms_search\b/)
277
+ gsub_file model_file_path, / scope :inline_forms_search, ->.*\n/, new_scope
278
+ else
279
+ inject_into_class model_file_path, model_class_name, new_scope
280
+ end
281
+ end
282
+ end
283
+ end
284
+
285
+ module InlineForms
286
+ # Backwards-compatible alias (tests and any programmatic invokers that
287
+ # addressed the namespaced version while the generator was scoped to
288
+ # `module InlineForms`). Rails generator discovery uses the top-level
289
+ # constant above.
290
+ InlineFormsAddtoGenerator = ::InlineFormsAddtoGenerator unless const_defined?(:InlineFormsAddtoGenerator, false)
291
+ end
@@ -0,0 +1,85 @@
1
+ # -*- encoding : utf-8 -*-
2
+ require "rails/generators"
3
+ require "rails/generators/generated_attribute"
4
+ require "inline_forms"
5
+
6
+ # Shared `Rails::Generators::GeneratedAttribute` extensions used by both
7
+ # `InlineForms::InlineFormsGenerator` (`rails g inline_forms`) and
8
+ # `InlineForms::InlineFormsAddtoGenerator` (`rails g inline_forms_addto`).
9
+ #
10
+ # Loaded once; idempotent (the `class_eval` re-defines methods to the
11
+ # same bodies on a re-require, no behavior drift).
12
+ Rails::Generators::GeneratedAttribute.class_eval do
13
+ # Override Rails::Generators::GeneratedAttribute.valid_type? so that our
14
+ # custom field types (dropdown, check_list, image_field, rich_text, ...)
15
+ # pass through parsing. We do our own unknown-type detection later (with
16
+ # Thor::Error + --allow-unknown), so it is safe to accept everything here.
17
+ #
18
+ # Rails 6.1 used to rescue ActiveRecord::Base.connection failures, which
19
+ # masked the issue; Rails 7+ raises NameError when ActiveRecord is not
20
+ # loaded yet, breaking generator unit tests.
21
+ def self.valid_type?(_type)
22
+ true
23
+ end
24
+
25
+ # Deducts the column_type for migrations from the type.
26
+ #
27
+ # We first merge the Special Column Types with the Default Column Types,
28
+ # which has the effect that the Default Column Types with the same key
29
+ # override the Special Column Types.
30
+ #
31
+ # If the type is not in the merged hash, then column_type defaults to :unknown
32
+ #
33
+ # You are advised to check your migrations for the :unknown, because either you made a
34
+ # typo in the generator command line or you need to add a Form Element!
35
+ def column_type
36
+ InlineForms::SPECIAL_COLUMN_TYPES
37
+ .merge(InlineForms::DEFAULT_COLUMN_TYPES)
38
+ .merge(InlineForms::RELATIONS)
39
+ .merge(InlineForms::SPECIAL_RELATIONS)[type] || :unknown
40
+ end
41
+
42
+ # Override the attribute_type to include our special column types.
43
+ #
44
+ # If a type is not in the Special Column Type hash, then the default
45
+ # column type hash is used, and if that fails, the attribute_type
46
+ # will be :unknown. Make sure to check your models for the :unknown.
47
+ def attribute_type
48
+ if InlineForms::SPECIAL_COLUMN_TYPES.merge(InlineForms::RELATIONS).has_key?(type)
49
+ type
50
+ else
51
+ InlineForms::DEFAULT_FORM_ELEMENTS[type] || :unknown
52
+ end
53
+ end
54
+
55
+ def special_relation?
56
+ InlineForms::SPECIAL_RELATIONS.has_key?(type)
57
+ end
58
+
59
+ def relation?
60
+ InlineForms::RELATIONS.has_key?(type) || special_relation?
61
+ end
62
+
63
+ # Special "attribute" names that drive the generator (presentation,
64
+ # ordering, search, etc.) but never become real columns or fields.
65
+ SPECIAL_GENERATOR_NAMES = %w[
66
+ _presentation
67
+ _order
68
+ _list_order
69
+ _list_search
70
+ _enabled
71
+ _id
72
+ _no_migration
73
+ _no_model
74
+ ].freeze unless const_defined?(:SPECIAL_GENERATOR_NAMES)
75
+
76
+ def migration?
77
+ not ( column_type == :no_migration ||
78
+ SPECIAL_GENERATOR_NAMES.include?(name) )
79
+ end
80
+
81
+ def attribute?
82
+ not ( SPECIAL_GENERATOR_NAMES.include?(name) ||
83
+ relation? )
84
+ end
85
+ end
@@ -1,5 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  require "inline_forms"
3
+ require_relative "inline_forms_attribute_overrides"
3
4
  module InlineForms
4
5
  # == Usage
5
6
  # This generator generates a migration, a model and a controller.
@@ -13,82 +14,12 @@ module InlineForms
13
14
  # rails g example_generator Modelname attribute:type attribute:type ...
14
15
  # an array with attributes and types is created for use in the generator.
15
16
  #
16
- # Rails::Generators::GeneratedAttribute creates, among others, a attribute_type.
17
+ # Rails::Generators::GeneratedAttribute creates, among others, an attribute_type.
17
18
  # This attribute_type maps column types to form attribute helpers like text_field.
18
- # We override it here to make our own.
19
+ # We override it in `lib/generators/inline_forms_attribute_overrides.rb`
20
+ # (shared with `InlineFormsAddtoGenerator`).
19
21
  #
20
22
  class InlineFormsGenerator < Rails::Generators::NamedBase
21
- Rails::Generators::GeneratedAttribute.class_eval do #:doc:
22
- # Override Rails::Generators::GeneratedAttribute.valid_type? so that our
23
- # custom field types (dropdown, check_list, image_field, rich_text, ...)
24
- # pass through parsing. We do our own unknown-type detection later (with
25
- # Thor::Error + --allow-unknown), so it is safe to accept everything here.
26
- #
27
- # Rails 6.1 used to rescue ActiveRecord::Base.connection failures, which
28
- # masked the issue; Rails 7+ raises NameError when ActiveRecord is not
29
- # loaded yet, breaking generator unit tests.
30
- def self.valid_type?(_type)
31
- true
32
- end
33
-
34
- # Deducts the column_type for migrations from the type.
35
- #
36
- # We first merge the Special Column Types with the Default Column Types,
37
- # which has the effect that the Default Column Types with the same key override
38
- # the Special Column Types.
39
- #
40
- # If the type is not in the merged hash, then column_type defaults to :unknown
41
- #
42
- # You are advised to check you migrations for the :unknown, because either you made a
43
- # typo in the generator command line or you need to add a Form Element!
44
- #
45
- def column_type
46
- SPECIAL_COLUMN_TYPES.merge(DEFAULT_COLUMN_TYPES).merge(RELATIONS).merge(SPECIAL_RELATIONS)[type] || :unknown
47
- end
48
-
49
- # Override the attribute_type to include our special column types.
50
- #
51
- # If a type is not in the Special Column Type hash, then the default
52
- # column type hash is used, and if that fails, the attribute_type
53
- # will be :unknown. Make sure to check your models for the :unknown.
54
- #
55
- def attribute_type
56
- SPECIAL_COLUMN_TYPES.merge(RELATIONS).has_key?(type) ? type : DEFAULT_FORM_ELEMENTS[type] || :unknown
57
- end
58
-
59
- def special_relation?
60
- SPECIAL_RELATIONS.has_key?(type)
61
- end
62
-
63
- def relation?
64
- RELATIONS.has_key?(type) || special_relation?
65
- end
66
-
67
- # Special "attribute" names that drive the generator (presentation,
68
- # ordering, search, etc.) but never become real columns or fields.
69
- SPECIAL_GENERATOR_NAMES = %w[
70
- _presentation
71
- _order
72
- _list_order
73
- _list_search
74
- _enabled
75
- _id
76
- _no_migration
77
- _no_model
78
- ].freeze
79
-
80
- def migration?
81
- not ( column_type == :no_migration ||
82
- SPECIAL_GENERATOR_NAMES.include?(name) )
83
- end
84
-
85
- def attribute?
86
- not ( SPECIAL_GENERATOR_NAMES.include?(name) ||
87
- relation? )
88
- end
89
-
90
-
91
- end
92
23
  argument :attributes, :type => :array, :banner => "[name:form_element]..."
93
24
  class_option :allow_unknown, :type => :boolean, :default => false, :desc => "Allow unknown field types (legacy behavior: comment generated lines instead of failing)."
94
25
 
@@ -0,0 +1,7 @@
1
+ class <%= migration_class_name %> < ActiveRecord::Migration[8.1]
2
+
3
+ def change
4
+ <%= @migration_lines -%>
5
+ end
6
+
7
+ end
@@ -1,4 +1,4 @@
1
- class InlineFormsCreate<%= table_name.camelize %> < ActiveRecord::Migration[8.0]
1
+ class InlineFormsCreate<%= table_name.camelize %> < ActiveRecord::Migration[8.1]
2
2
 
3
3
  def self.up
4
4
  create_table :<%= table_name + @primary_key_option %> do |t|
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module InlineForms
3
- VERSION = "8.1.0"
3
+ VERSION = "8.1.2"
4
4
  end
@@ -0,0 +1,251 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+ require "logger"
7
+ require "rails"
8
+ require "rails/generators"
9
+ require "inline_forms"
10
+ require_relative "../lib/generators/inline_forms_addto_generator"
11
+
12
+ class InlineFormsAddtoGeneratorTest < Minitest::Test
13
+ GENERATOR_SHAPED_MODEL = <<~RUBY
14
+ class Widget < ApplicationRecord
15
+
16
+ belongs_to :category
17
+
18
+ def inline_forms_attribute_list
19
+ @inline_forms_attribute_list ||= [
20
+ [ :name , "name", :text_field ],
21
+ [ :category , "category", :belongs_to ],
22
+ ]
23
+ end
24
+
25
+ end
26
+ RUBY
27
+
28
+ INSTALLER_SHAPED_USER = <<~RUBY
29
+ class User < ApplicationRecord
30
+
31
+ devise :database_authenticatable
32
+ belongs_to :locale
33
+ has_and_belongs_to_many :roles
34
+
35
+ def _presentation
36
+ "\#{name}"
37
+ end
38
+
39
+ def inline_forms_attribute_list
40
+ @inline_forms_attribute_list ||= [
41
+ [ :header_user_login, '', :header ],
42
+ [ :name, '', :text_field ],
43
+ [ :email, '', :text_field ],
44
+ [ :locale , '', :dropdown ],
45
+ [ :password, '', :devise_password_field ],
46
+ [ :header_user_roles, '', :header ],
47
+ [ :roles, '', :check_list ],
48
+ ]
49
+ end
50
+
51
+ end
52
+ RUBY
53
+
54
+ BARE_MODEL = <<~RUBY
55
+ class Bare < ApplicationRecord
56
+ end
57
+ RUBY
58
+
59
+ def setup
60
+ @destination_root = Dir.mktmpdir("inline_forms_addto_generator_test")
61
+ mkdir_p("app/models")
62
+ mkdir_p("db/migrate")
63
+ end
64
+
65
+ def teardown
66
+ FileUtils.remove_entry(@destination_root) if @destination_root && Dir.exist?(@destination_root)
67
+ end
68
+
69
+ def test_raises_when_model_file_missing
70
+ stdout, stderr = capture_io do
71
+ run_generator("DoesNotExist", "occupation:string")
72
+ end
73
+ output = "#{stdout}#{stderr}"
74
+ assert_includes(output, "rails g inline_forms DoesNotExist")
75
+ refute(File.exist?(File.join(@destination_root, "app/models/does_not_exist.rb")))
76
+ end
77
+
78
+ def test_adds_scalar_column_belongs_to_rich_text_and_image_field_to_existing_model
79
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
80
+
81
+ run_generator(
82
+ "Widget",
83
+ "occupation:string",
84
+ "organization:belongs_to",
85
+ "supplier:dropdown",
86
+ "bio:rich_text",
87
+ "avatar:image_field"
88
+ )
89
+
90
+ model = read("app/models/widget.rb")
91
+ migration = read_single_addto_migration_for("widgets")
92
+
93
+ assert_includes(model, "belongs_to :organization")
94
+ assert_includes(model, "belongs_to :supplier")
95
+ assert_includes(model, "has_rich_text :bio")
96
+ assert_includes(model, "mount_uploader :avatar, AvatarUploader")
97
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
98
+ # :belongs_to is a relation -> no row in inline_forms_attribute_list
99
+ # (matches InlineFormsGenerator semantics). :dropdown is not a relation
100
+ # at lookup time, so it does get a row.
101
+ refute_includes(model, '[ :organization , "organization", :belongs_to ]')
102
+ assert_includes(model, '[ :supplier , "supplier", :dropdown ]')
103
+ assert_includes(model, '[ :avatar , "avatar", :image_field ]')
104
+
105
+ assert_includes(migration, "add_column :widgets, :occupation, :string")
106
+ assert_includes(migration, "add_reference :widgets, :organization, foreign_key: true")
107
+ assert_includes(migration, "add_reference :widgets, :supplier, foreign_key: true")
108
+ assert_includes(migration, "add_column :widgets, :avatar, :string")
109
+ refute_includes(migration, ":bio")
110
+ refute_includes(migration, "create_table")
111
+ end
112
+
113
+ def test_idempotent_rerun_does_not_duplicate_lines
114
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
115
+
116
+ run_generator("Widget", "occupation:string", "organization:belongs_to")
117
+ sleep 1 # unique migration timestamp on second run
118
+ run_generator("Widget", "occupation:string", "organization:belongs_to")
119
+
120
+ model = read("app/models/widget.rb")
121
+
122
+ assert_equal(1, model.scan("belongs_to :organization").size)
123
+ assert_equal(1, model.scan('[ :occupation , "occupation", :text_field ]').size)
124
+ end
125
+
126
+ def test_appends_row_to_installer_shaped_user_attribute_list
127
+ write_model("user.rb", INSTALLER_SHAPED_USER)
128
+
129
+ run_generator("User", "occupation:string", "birthdate:date")
130
+
131
+ model = read("app/models/user.rb")
132
+
133
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
134
+ assert_includes(model, '[ :birthdate , "birthdate", :date_select ]')
135
+ refute_match(/\]\s*\n\s*\[\s*:occupation/, model)
136
+
137
+ user_array_section = model[/@inline_forms_attribute_list \|\|=\s*\[(.|\n)*?\n\s*\]/]
138
+ assert(user_array_section, "could not locate @inline_forms_attribute_list array")
139
+ assert_match(/\[ :roles,.*\].*?\[ :occupation\b/m, user_array_section)
140
+ end
141
+
142
+ def test_bare_model_gets_fresh_attribute_list_method
143
+ write_model("bare.rb", BARE_MODEL)
144
+
145
+ out, _err = capture_io do
146
+ run_generator("Bare", "occupation:string")
147
+ end
148
+
149
+ model = read("app/models/bare.rb")
150
+
151
+ assert_includes(out, "no inline_forms_attribute_list found")
152
+ assert_includes(model, "def inline_forms_attribute_list")
153
+ assert_includes(model, '[ :occupation , "occupation", :text_field ]')
154
+ end
155
+
156
+ def test_unknown_type_raises_thor_error_by_default
157
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
158
+
159
+ stdout, stderr = capture_io do
160
+ run_generator("Widget", "payload:not_a_real_type")
161
+ end
162
+ output = "#{stdout}#{stderr}"
163
+ assert_includes(output, "Unknown field type(s): payload:not_a_real_type")
164
+ assert_includes(output, "--allow-unknown")
165
+
166
+ refute_addto_migration_for("widgets")
167
+ end
168
+
169
+ def test_allow_unknown_keeps_legacy_commented_output
170
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
171
+
172
+ run_generator("Widget", "payload:not_a_real_type", "--allow-unknown")
173
+
174
+ model = read("app/models/widget.rb")
175
+ migration = read_single_addto_migration_for("widgets")
176
+
177
+ assert_includes(model, '[ :payload , "payload", :unknown ]')
178
+ assert_includes(migration, "# add_column :widgets, :payload, :unknown")
179
+ end
180
+
181
+ def test_install_time_only_names_are_rejected
182
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
183
+
184
+ %w[_no_model _no_migration _id _enabled].each do |forbidden|
185
+ stdout, stderr = capture_io do
186
+ run_generator("Widget", "#{forbidden}:yes")
187
+ end
188
+ output = "#{stdout}#{stderr}"
189
+ assert_includes(output, forbidden, "expected error to mention #{forbidden}")
190
+ assert_includes(output, "install-time only")
191
+ end
192
+ end
193
+
194
+ def test_skips_replace_only_names_without_replace_flag
195
+ write_model("widget.rb", GENERATOR_SHAPED_MODEL)
196
+
197
+ out, _err = capture_io do
198
+ run_generator("Widget", "_list_order:title")
199
+ end
200
+
201
+ assert_includes(out, "_list_order:title")
202
+ assert_includes(out, "skipped")
203
+ model = read("app/models/widget.rb")
204
+ refute_includes(model, "scope :inline_forms_list, -> { order(:title, :id) }")
205
+ end
206
+
207
+ def test_replace_flag_rewrites_list_order_scope
208
+ initial = GENERATOR_SHAPED_MODEL.sub(
209
+ /class Widget < ApplicationRecord\n/,
210
+ "class Widget < ApplicationRecord\n\n scope :inline_forms_list, -> { order(:name, :id) }\n def <=>(other)\n self.name <=> other.name\n end\n"
211
+ )
212
+ write_model("widget.rb", initial)
213
+
214
+ run_generator("Widget", "_list_order:title", "--replace")
215
+
216
+ model = read("app/models/widget.rb")
217
+ assert_includes(model, "scope :inline_forms_list, -> { order(:title, :id) }")
218
+ refute_includes(model, "order(:name, :id)")
219
+ assert_includes(model, "self.title <=> other.title")
220
+ refute_includes(model, "self.name <=> other.name")
221
+ end
222
+
223
+ private
224
+
225
+ def run_generator(*args)
226
+ InlineForms::InlineFormsAddtoGenerator.start(args, destination_root: @destination_root)
227
+ end
228
+
229
+ def write_model(file, content)
230
+ File.write(File.join(@destination_root, "app/models", file), content)
231
+ end
232
+
233
+ def read(relative_path)
234
+ File.read(File.join(@destination_root, relative_path))
235
+ end
236
+
237
+ def mkdir_p(relative_path)
238
+ FileUtils.mkdir_p(File.join(@destination_root, relative_path))
239
+ end
240
+
241
+ def read_single_addto_migration_for(table_name)
242
+ files = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_add_to_#{table_name}_*.rb"))
243
+ assert_equal(1, files.size, "Expected one addto migration for #{table_name}, got #{files.inspect}")
244
+ File.read(files.first)
245
+ end
246
+
247
+ def refute_addto_migration_for(table_name)
248
+ files = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_add_to_#{table_name}_*.rb"))
249
+ assert_equal([], files, "Expected no addto migration for #{table_name}")
250
+ end
251
+ end
@@ -56,7 +56,7 @@ class InlineFormsGeneratorTest < Minitest::Test
56
56
 
57
57
  assert_includes(application_controller, "MODEL_TABS = %w(things ")
58
58
 
59
- assert_includes(migration, "class InlineFormsCreateThings < ActiveRecord::Migration[8.0]")
59
+ assert_includes(migration, "class InlineFormsCreateThings < ActiveRecord::Migration[8.1]")
60
60
  assert_includes(migration, "create_table :things do |t|")
61
61
  assert_includes(migration, "t.string :name")
62
62
  assert_includes(migration, "t.belongs_to :category")
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inline_forms
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.0
4
+ version: 8.1.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -17,7 +17,7 @@ dependencies:
17
17
  requirements:
18
18
  - - ">="
19
19
  - !ruby/object:Gem::Version
20
- version: 8.1.0
20
+ version: 8.1.2
21
21
  - - "<"
22
22
  - !ruby/object:Gem::Version
23
23
  version: '9.0'
@@ -27,7 +27,7 @@ dependencies:
27
27
  requirements:
28
28
  - - ">="
29
29
  - !ruby/object:Gem::Version
30
- version: 8.1.0
30
+ version: 8.1.2
31
31
  - - "<"
32
32
  - !ruby/object:Gem::Version
33
33
  version: '9.0'
@@ -37,27 +37,27 @@ dependencies:
37
37
  requirements:
38
38
  - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '8.0'
40
+ version: '8.1'
41
41
  - - "<"
42
42
  - !ruby/object:Gem::Version
43
- version: '8.1'
43
+ version: '8.2'
44
44
  type: :runtime
45
45
  prerelease: false
46
46
  version_requirements: !ruby/object:Gem::Requirement
47
47
  requirements:
48
48
  - - ">="
49
49
  - !ruby/object:Gem::Version
50
- version: '8.0'
50
+ version: '8.1'
51
51
  - - "<"
52
52
  - !ruby/object:Gem::Version
53
- version: '8.1'
53
+ version: '8.2'
54
54
  - !ruby/object:Gem::Dependency
55
55
  name: rails-i18n
56
56
  requirement: !ruby/object:Gem::Requirement
57
57
  requirements:
58
58
  - - ">="
59
59
  - !ruby/object:Gem::Version
60
- version: '8.0'
60
+ version: '8.1'
61
61
  - - "<"
62
62
  - !ruby/object:Gem::Version
63
63
  version: '9.0'
@@ -67,7 +67,7 @@ dependencies:
67
67
  requirements:
68
68
  - - ">="
69
69
  - !ruby/object:Gem::Version
70
- version: '8.0'
70
+ version: '8.1'
71
71
  - - "<"
72
72
  - !ruby/object:Gem::Version
73
73
  version: '9.0'
@@ -86,7 +86,7 @@ dependencies:
86
86
  - !ruby/object:Gem::Version
87
87
  version: '5.0'
88
88
  description: Inline Forms eases setup of admin-style forms with inline editing. Field
89
- lists are declared on the model. Requires Rails 8.0.x, Ruby >= 4.0, and validation_hints
89
+ lists are declared on the model. Requires Rails 8.1.x, Ruby >= 4.0, and validation_hints
90
90
  ~> 8.
91
91
  email:
92
92
  - ace@suares.com
@@ -495,8 +495,11 @@ files:
495
495
  - inline_forms.gemspec
496
496
  - lib/generators/USAGE
497
497
  - lib/generators/assets/stylesheets/inline_forms_devise.css
498
+ - lib/generators/inline_forms_addto_generator.rb
499
+ - lib/generators/inline_forms_attribute_overrides.rb
498
500
  - lib/generators/inline_forms_generator.rb
499
501
  - lib/generators/templates/_inline_forms_tabs.html.erb
502
+ - lib/generators/templates/add_columns_migration.erb
500
503
  - lib/generators/templates/application_record.rb
501
504
  - lib/generators/templates/controller.erb
502
505
  - lib/generators/templates/migration.erb
@@ -558,6 +561,7 @@ files:
558
561
  - test/archived_form_elements_test.rb
559
562
  - test/form_element_from_callee_test.rb
560
563
  - test/inline_edit_polymorphic_path_test.rb
564
+ - test/inline_forms_addto_generator_test.rb
561
565
  - test/inline_forms_generator_test.rb
562
566
  - test/plain_text_configuration_test.rb
563
567
  - test/test_helper.rb
@@ -590,6 +594,7 @@ test_files:
590
594
  - test/archived_form_elements_test.rb
591
595
  - test/form_element_from_callee_test.rb
592
596
  - test/inline_edit_polymorphic_path_test.rb
597
+ - test/inline_forms_addto_generator_test.rb
593
598
  - test/inline_forms_generator_test.rb
594
599
  - test/plain_text_configuration_test.rb
595
600
  - test/test_helper.rb