inline_forms 8.0.4 → 8.1.1

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: 83fc3ebab88e414806da9e2c7f9a47088eb1d69579f607af783e647f374ada2b
4
- data.tar.gz: 76348168e6d35fb88a7142e66e92938ae6938b83cd606a894f1985c37c7cd219
3
+ metadata.gz: 86869c3131530e4764f1a16620e48bd0eaf920461f0471697c07d2e30bda69f3
4
+ data.tar.gz: 5c304a602f87fcc37b403d3b2299b5f467da1eef65f1fde8d624cadc8085b1c9
5
5
  SHA512:
6
- metadata.gz: d3d2b9b5a2cc0239857123cf16cd6605f58d27e324accddb8232015445767e3b06214c4c869d6089218b42aa72f737a11611d83abc3b53e731573ea628987944
7
- data.tar.gz: 29d10dd86bb1c3bff98a5653b513de213e08250ab43b3fd3969e1aeaa71cd690f73f9034f51c64e775debeff69abd6180b5895867e7c4ca0a9aa10bda879fe07
6
+ metadata.gz: 940a25c0f1ac072dbc7c1bd098e72f37e81b5f24e96230e399bbf0d4e22eba3260177ce2029751ada4c572d9e04585dafd97cfac7513ae9848c8deb5c91ed8a4
7
+ data.tar.gz: 372c3f53240ded87cee02386f6b0b52586c9abb5fec8889b2968483383f4f01c09483f408a06eb6a1ccb0716c5b01ab4db3d6dfb9e76cc72f555a16739ca3f07
data/CHANGELOG.md CHANGED
@@ -4,6 +4,54 @@ All notable changes to this project are documented in this file.
4
4
 
5
5
  ## [Unreleased]
6
6
 
7
+ ## [8.1.1] - 2026-05-26
8
+
9
+ ### Added
10
+
11
+ - **`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).
12
+ - **`--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 ]`).
13
+ - **`--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.
14
+
15
+ ### Changed
16
+
17
+ - **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.
18
+
19
+ ### Notes
20
+
21
+ - 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.
22
+
23
+ ## [8.1.0] - 2026-05-25
24
+
25
+ ### Added
26
+
27
+ - **`ApplicationRecord` template carries shared model behavior:** `has_paper_trail on: [:create, :update, :destroy]`, `attr_writer :inline_forms_attribute_list`, `self.per_page = 7`, `human_attribute_name` instance wrapper, and `def self.not_accessible_through_html?` default. Generated models inherit all of this; `lib/generators/templates/model.erb` only emits per-model differences (associations, `inline_forms_attribute_list`, `_presentation`, `<=>`, `not_accessible_through_html?` override when `_enabled` is absent).
28
+ - **`scope :inline_forms_list, -> { all }` and `scope :inline_forms_search, ->(_q) { all }`** on `ApplicationRecord`. Both are no-ops by default; subclasses opt in. Replaces the old `def self.order_by_clause` contract with composable Active Record relations.
29
+ - **Generator flags `_list_order:col` and `_list_search:col`:** emit `scope :inline_forms_list, -> { order(:col, :id) }` and `scope :inline_forms_search, ->(q) { where("col LIKE ?", "%#{q}%") }` respectively. `_list_order` also still emits `def <=>(other)` for in-memory sort on associations (used by `check_list` show). Tie-break on `:id` is added automatically so will_paginate pages stay stable.
30
+ - **`SPECIAL_GENERATOR_NAMES` constant** in `InlineForms::InlineFormsGenerator` consolidates `_presentation`, `_order`, `_list_order`, `_list_search`, `_enabled`, `_id`, `_no_migration`, `_no_model` so `migration?` / `attribute?` no longer drift.
31
+
32
+ ### Changed
33
+
34
+ - **`order_by_clause` is gone from the gem.** `InlineFormsController#index` / `#create`, `check_list_helper.rb`, and `app/views/inline_forms/_list.html.erb` now compose `merge(@Klass.inline_forms_list)` and `merge(@Klass.inline_forms_search(params[:search]))` instead of building raw `"table.col"` strings. Search and order are now independent (one column had to do both jobs before); models can declare each separately or neither.
35
+ - **Per-page pagination fix in `ApplicationRecord`:** `self.per_page = 7` (class-level, what will_paginate's `class_attribute :per_page` reads). The old `attr_reader :per_page` + `@per_page = 7` instance pair on every generated model was a no-op (described at length in 8.0.x notes); models that need a different value override with `self.per_page = N` (Photo uses `5`). The installer’s Photo override now uses `inject_into_class "app/models/photo.rb", "Photo", " self.per_page = 5\n"` instead of patching the legacy `attr_reader` block out.
36
+ - **`_order:col` is deprecated as an alias for `_list_order:col`.** Still works (emits the same scope plus `<=>`); the generator prints `say_status :deprecated, "_order:col is deprecated; use _list_order:col"` so existing scripts keep generating but the migration path is visible.
37
+ - **Installer User model:** dropped `default_scope { order :name }` and the `def self.order_by_clause; nil; end` stub. Added `scope :inline_forms_list, -> { order(:name, :id) }` with a comment about why named scopes beat `default_scope` (no `unscope`/`reorder` foot-guns; cleaner `update_all` / association inheritance).
38
+ - **Installer example-app generator calls** for `Locale`, `Role`, `Photo`, `Apartment`, `Owner` now pass `_list_order:name` (and `_list_search:name` for the searchable top-level models `Apartment` and `Owner`) to declare ordering and search explicitly. Replaces the implicit `order_by_clause = "name"` default that the old generator produced.
39
+
40
+ ### Removed
41
+
42
+ - **`lib/generators/assets/javascripts/`** (including the empty `ckeditor/` subfolder). Leftover from the CKEditor era; nothing in the gem or installer copied from it. The engine still ships JS under `app/assets/javascripts/` and `vendor/assets/javascripts/`.
43
+ - **`lib/generators/assets/stylesheets/inline_forms.scss`** mirror. The installer doesn't copy it; the gem stylesheet at `app/assets/stylesheets/inline_forms/inline_forms.scss` is the single source consumed via `@use "inline_forms/inline_forms"`. `inline_forms_devise.css` stays — it is the host-app override hook copied by the installer and linked from `layouts/devise.html.erb`.
44
+
45
+ ### Migration notes
46
+
47
+ Existing apps upgrading from 8.0.x:
48
+
49
+ 1. Replace your `app/models/application_record.rb` with the contents of `lib/generators/templates/application_record.rb` (or just merge in the new `has_paper_trail`, `self.per_page`, `inline_forms_list` / `inline_forms_search` scopes, and `not_accessible_through_html?` default).
50
+ 2. For each model that had `def self.order_by_clause; "col"; end`, replace with `scope :inline_forms_list, -> { order(:col, :id) }`. Drop the old method; the gem no longer reads it.
51
+ 3. If you used `default_scope { order(:name) }` for inline_forms list ordering, swap to the named scope above.
52
+ 4. Per-model `attr_reader :per_page` / `@per_page = N` pairs are no-ops; replace with `self.per_page = N` (or remove and inherit `7` from `ApplicationRecord`).
53
+ 5. `_order:col` in your generator scripts still works but emits a deprecation notice; switch to `_list_order:col` and add `_list_search:col` if you want the search box to filter on that column.
54
+
7
55
  ## [8.0.4] - 2026-05-24
8
56
 
9
57
  ### Added
@@ -36,14 +36,12 @@ class InlineFormsController < ApplicationController
36
36
  @parent_class = params[:parent_class]
37
37
  @parent_id = params[:parent_id]
38
38
  @ul_needed = params[:ul_needed]
39
- # if the parent_class is not nill, we are in associated list and we don't search there.
40
- # also, make sure the Model that you want to do a search on has a :name attribute. TODO
41
- conditions = nil
42
- if @parent_class.nil? || @Klass.reflect_on_association(@parent_class.underscore.to_sym).nil?
43
- conditions = [ @Klass.table_name + "." + @Klass.order_by_clause + " like ?", "%#{params[:search]}%" ] if @Klass.respond_to?(:order_by_clause) && ! @Klass.order_by_clause.nil?
44
- else
39
+ # Nested associated lists scope to the parent FK. Top-level lists may
40
+ # apply the model's `inline_forms_search` scope when ?search= is passed.
41
+ fk_conditions = nil
42
+ if @parent_class.present? && @Klass.reflect_on_association(@parent_class.underscore.to_sym)
45
43
  foreign_key = @Klass.reflect_on_association(@parent_class.underscore.to_sym).options[:foreign_key] || @parent_class.foreign_key
46
- conditions = [ "#{foreign_key} = ?", @parent_id ]
44
+ fk_conditions = [ "#{foreign_key} = ?", @parent_id ]
47
45
  end
48
46
  # CanCan's load_and_authorize_resource sets @apartments (etc.); keep @objects in sync.
49
47
  collection_ivar = :"@#{controller_name}"
@@ -52,9 +50,13 @@ class InlineFormsController < ApplicationController
52
50
  @objects = loaded unless loaded.nil?
53
51
  end
54
52
  @objects ||= @Klass.accessible_by(current_ability) if cancan_enabled?
55
- @objects ||= @Klass
56
- @objects = @objects.order(@Klass.table_name + "." + @Klass.order_by_clause) if @Klass.respond_to?(:order_by_clause) && ! @Klass.order_by_clause.nil?
57
- @objects = @objects.where(conditions).paginate(:page => params[:page])
53
+ @objects ||= @Klass.all
54
+ @objects = @objects.merge(@Klass.inline_forms_list) if @Klass.respond_to?(:inline_forms_list)
55
+ if fk_conditions.nil? && params[:search].present? && @Klass.respond_to?(:inline_forms_search)
56
+ @objects = @objects.merge(@Klass.inline_forms_search(params[:search]))
57
+ end
58
+ @objects = @objects.where(fk_conditions) if fk_conditions
59
+ @objects = @objects.paginate(:page => params[:page])
58
60
  respond_to do |format|
59
61
  # `not_accessible_through_html?` is about preventing direct top-level
60
62
  # HTML CRUD on this resource (e.g. /photos when only Apartment is the
@@ -139,22 +141,23 @@ class InlineFormsController < ApplicationController
139
141
  end
140
142
  @parent_class = params[:parent_class]
141
143
  @parent_id = params[:parent_id]
142
- # for the logic behind the :conditions see the #index method.
143
- conditions = nil
144
- if @parent_class.nil? || @Klass.reflect_on_association(@parent_class.underscore.to_sym).nil?
145
- conditions = [ @Klass.table_name + "." + @Klass.order_by_clause + " like ?", "%#{params[:search]}%" ] if @Klass.respond_to?(:order_by_clause) && ! @Klass.order_by_clause.nil?
146
- else
144
+ # See #index for the order/search/parent-fk decomposition.
145
+ fk_conditions = nil
146
+ if @parent_class.present? && @Klass.reflect_on_association(@parent_class.underscore.to_sym)
147
147
  foreign_key = @Klass.reflect_on_association(@parent_class.underscore.to_sym).options[:foreign_key] || @parent_class.foreign_key
148
- conditions = [ "#{foreign_key} = ?", @parent_id ]
148
+ fk_conditions = [ "#{foreign_key} = ?", @parent_id ]
149
149
  @object[foreign_key] = @parent_id
150
150
  end
151
151
 
152
152
  if @object.save
153
153
  flash.now[:success] = t('success', :message => @object.class.model_name.human)
154
- @objects = @Klass
155
- @objects = @Klass.accessible_by(current_ability) if cancan_enabled?
156
- @objects = @objects.order(@Klass.table_name + "." + @Klass.order_by_clause) if @Klass.respond_to?(:order_by_clause) && ! @Klass.order_by_clause.nil?
157
- @objects = @objects.where(conditions).paginate(:page => params[:page])
154
+ @objects = cancan_enabled? ? @Klass.accessible_by(current_ability) : @Klass.all
155
+ @objects = @objects.merge(@Klass.inline_forms_list) if @Klass.respond_to?(:inline_forms_list)
156
+ if fk_conditions.nil? && params[:search].present? && @Klass.respond_to?(:inline_forms_search)
157
+ @objects = @objects.merge(@Klass.inline_forms_search(params[:search]))
158
+ end
159
+ @objects = @objects.where(fk_conditions) if fk_conditions
160
+ @objects = @objects.paginate(:page => params[:page])
158
161
  @object = nil
159
162
  respond_to do |format|
160
163
  format.html { render_list_frame_after_save } if html_list_flow_allowed?
@@ -34,9 +34,10 @@
34
34
  <% foreign_key = parent_class.reflect_on_association(attribute.to_sym).options[:foreign_key] || parent_class.name.foreign_key -%>
35
35
  <% model = attribute.to_s.singularize.camelcase.constantize %>
36
36
  <% conditions = [ "#{model.table_name}.#{foreign_key} = ?", parent_id ] %>
37
+ <% klass = attribute.to_s.singularize.camelcase.constantize %>
37
38
  <% objects = parent_class.find(parent_id).send(attribute) %>
38
39
  <% objects = parent_class.find(parent_id).send(attribute).accessible_by(current_ability) if cancan_enabled? %>
39
- <% objects = objects.order(attribute.to_s.singularize.camelcase.constantize.order_by_clause) if attribute.to_s.singularize.camelcase.constantize.respond_to?(:order_by_clause) && ! attribute.to_s.singularize.camelcase.constantize.order_by_clause.nil? %>
40
+ <% objects = objects.merge(klass.inline_forms_list) if klass.respond_to?(:inline_forms_list) %>
40
41
  <% objects = objects.where(conditions).paginate(:page => params[:page]) %>
41
42
  <% end %>
42
43
  <% end %>
data/inline_forms.gemspec CHANGED
@@ -19,7 +19,7 @@ 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.0.4", "< 9.0")
22
+ s.add_dependency("validation_hints", ">= 8.1.0", "< 9.0")
23
23
  s.add_dependency("rails", ">= 8.0", "< 8.1")
24
24
  s.add_dependency("rails-i18n", ">= 8.0", "< 9.0")
25
25
 
@@ -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