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 +4 -4
- data/CHANGELOG.md +48 -0
- data/app/controllers/inline_forms_controller.rb +23 -20
- data/app/views/inline_forms/_list.html.erb +2 -1
- data/inline_forms.gemspec +1 -1
- data/lib/generators/inline_forms_addto_generator.rb +291 -0
- data/lib/generators/inline_forms_attribute_overrides.rb +85 -0
- data/lib/generators/inline_forms_generator.rb +18 -75
- data/lib/generators/templates/add_columns_migration.erb +7 -0
- data/lib/generators/templates/application_record.rb +32 -0
- data/lib/generators/templates/model.erb +7 -14
- data/lib/inline_forms/form_elements/check_list_helper.rb +3 -5
- data/lib/inline_forms/version.rb +1 -1
- data/test/inline_forms_addto_generator_test.rb +251 -0
- data/test/inline_forms_generator_test.rb +61 -0
- metadata +8 -13
- data/docs/git-deps-assessment.md +0 -93
- data/docs/jquery-widgets.md +0 -25
- data/docs/prompt/.gitignore +0 -5
- data/docs/prompt/test-the-example-app.md +0 -32
- data/docs/rails-8-phase4-audit.md +0 -39
- data/docs/rails-8-release.md +0 -56
- data/docs/turbo-stream-audit.md +0 -16
- data/docs/ujs-to-turbo.md +0 -207
- data/docs/zeitwerk-and-load-paths.md +0 -49
- data/lib/generators/assets/stylesheets/inline_forms.scss +0 -491
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 86869c3131530e4764f1a16620e48bd0eaf920461f0471697c07d2e30bda69f3
|
|
4
|
+
data.tar.gz: 5c304a602f87fcc37b403d3b2299b5f467da1eef65f1fde8d624cadc8085b1c9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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
|
-
#
|
|
40
|
-
#
|
|
41
|
-
|
|
42
|
-
if @parent_class.
|
|
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
|
-
|
|
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.
|
|
57
|
-
|
|
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
|
-
#
|
|
143
|
-
|
|
144
|
-
if @parent_class.
|
|
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
|
-
|
|
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.
|
|
156
|
-
|
|
157
|
-
|
|
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.
|
|
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
|
|
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
|