inline_forms 8.1.40 → 8.1.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. checksums.yaml +4 -4
  2. data/.forgejo/workflows/full-gate.yml +4 -2
  3. data/.gitignore +6 -2
  4. data/.rubocop.yml +1 -0
  5. data/CHANGELOG.md +44 -0
  6. data/CLAUDE.md +68 -0
  7. data/Gemfile +4 -0
  8. data/Rakefile +52 -11
  9. data/app/controllers/inline_forms_controller.rb +15 -0
  10. data/app/views/inline_forms/_new.html.erb +4 -1
  11. data/app/views/inline_forms/_pending_migration_row.html.erb +15 -0
  12. data/app/views/inline_forms/_show.html.erb +4 -1
  13. data/app/views/inline_forms/field_pending.html.erb +10 -0
  14. data/lib/generators/inline_forms_addto/USAGE +31 -0
  15. data/lib/generators/inline_forms_addto_generator.rb +198 -26
  16. data/lib/inline_forms/attribute_list.rb +191 -0
  17. data/lib/inline_forms/gem_files.rb +26 -0
  18. data/lib/inline_forms/schema_apply.rb +85 -0
  19. data/lib/inline_forms/schema_intent.rb +76 -0
  20. data/lib/inline_forms/schema_label.rb +65 -0
  21. data/lib/inline_forms/schema_preview.rb +135 -0
  22. data/lib/inline_forms/version.rb +1 -1
  23. data/lib/inline_forms.rb +86 -0
  24. data/lib/locales/inline_forms.en.yml +2 -0
  25. data/lib/locales/inline_forms.nl.yml +2 -0
  26. data/test/attribute_list_test.rb +165 -0
  27. data/test/dummy/app/controllers/gizmos_controller.rb +4 -0
  28. data/test/dummy/app/models/gizmo.rb +25 -0
  29. data/test/dummy/config/application.rb +3 -0
  30. data/test/dummy/config/routes.rb +9 -0
  31. data/test/inline_forms_addto_generator_test.rb +220 -0
  32. data/test/integration/pending_migration_gate_test.rb +75 -0
  33. data/test/integration/schema_batch_pipeline_test.rb +189 -0
  34. data/test/integration/schema_edit_test.rb +141 -0
  35. data/test/integration/schema_preview_test.rb +65 -0
  36. data/test/integration_test_helper.rb +41 -0
  37. data/test/schema_apply_test.rb +73 -0
  38. data/test/schema_intent_test.rb +53 -0
  39. data/test/schema_label_test.rb +61 -0
  40. metadata +29 -1
@@ -0,0 +1,65 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../integration_test_helper"
4
+
5
+ # Cheap preview of a SchemaIntent against the dummy app: a virtual typed
6
+ # attribute on a throwaway subclass + an AttributeList with the proposed row
7
+ # spliced in — no migration, no column, real class/table untouched.
8
+ class SchemaPreviewTest < InlineFormsIntegrationTestCase
9
+ def scalar_intent(**overrides)
10
+ InlineForms::SchemaIntent.new(
11
+ **{ model_name: "Widget", attribute: :internal_note, form_element: :text_field }.merge(overrides)
12
+ )
13
+ end
14
+
15
+ test "supported? is true for scalar elements, false for relations/uploaders/virtuals" do
16
+ assert InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :text_field))
17
+ assert InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :integer_field))
18
+ assert InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :date_select))
19
+ assert InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :check_box))
20
+ refute InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :dropdown))
21
+ refute InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :rich_text))
22
+ refute InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :image_field))
23
+ refute InlineForms::SchemaPreview.supported?(scalar_intent(form_element: :money_field))
24
+ end
25
+
26
+ test "preview object carries the proposed attribute as a virtual, no column" do
27
+ object, = InlineForms::SchemaPreview.build(scalar_intent)
28
+
29
+ assert object.respond_to?(:internal_note), "preview should expose the virtual attribute"
30
+ assert_nil object.internal_note, "virtual attribute defaults to nil with no column"
31
+ object.internal_note = "boiler serviced"
32
+ assert_equal "boiler serviced", object.internal_note, "virtual attribute is writable in memory"
33
+
34
+ # The real class/table are untouched.
35
+ refute_includes Widget.column_names, "internal_note"
36
+ assert_equal "Widget", object.class.name, "preview masquerades as the base class"
37
+ end
38
+
39
+ test "preview attribute_list splices the row at --after" do
40
+ _, list = InlineForms::SchemaPreview.build(scalar_intent(after: :name))
41
+ assert_kind_of InlineForms::AttributeList, list
42
+ assert_equal :internal_note, list.names[list.index_of(:name) + 1]
43
+ # original rows are preserved
44
+ assert list.include_attribute?(:priority)
45
+ end
46
+
47
+ test "preview attribute_list appends when no anchor given" do
48
+ _, list = InlineForms::SchemaPreview.build(scalar_intent)
49
+ assert_equal :internal_note, list.names.last
50
+ end
51
+
52
+ test "preview seeds existing attributes from a base record" do
53
+ widget = Widget.create!(name: "Base", priority: 2)
54
+ object, = InlineForms::SchemaPreview.build(scalar_intent, widget)
55
+ assert_equal "Base", object.name, "existing data is carried into the preview"
56
+ assert_nil object.internal_note
57
+ end
58
+
59
+ test "building a preview does not add a column to the real table" do
60
+ before = Widget.column_names.dup
61
+ InlineForms::SchemaPreview.build(scalar_intent(after: :name))
62
+ Widget.reset_column_information
63
+ assert_equal before, Widget.column_names
64
+ end
65
+ end
@@ -49,6 +49,44 @@ ActiveRecord::Schema.define do
49
49
  end
50
50
  add_index :parts, :machine_id
51
51
 
52
+ # Gizmo intentionally has NO `pending_note` column: its attribute_list
53
+ # references one, modeling the inline_forms_addto pre-migrate window that
54
+ # InlineForms.attribute_pending_migration? gates. See
55
+ # test/integration/pending_migration_gate_test.rb.
56
+ create_table :gizmos, force: true do |t|
57
+ t.string :name
58
+ t.timestamps
59
+ end
60
+
61
+ # Schema-GUI batch pipeline (inline_forms_schema_edit). Mirrors the gem's
62
+ # install-generator migration.
63
+ create_table :inline_forms_schema_batches, force: true do |t|
64
+ t.string :status, null: false, default: "draft"
65
+ t.string :requested_by
66
+ t.datetime :submitted_at
67
+ t.datetime :window_at
68
+ t.datetime :applied_at
69
+ t.string :git_sha
70
+ t.string :content_digest
71
+ t.text :error
72
+ t.timestamps
73
+ end
74
+ add_index :inline_forms_schema_batches, :status
75
+
76
+ create_table :inline_forms_schema_intents, force: true do |t|
77
+ t.references :batch, null: false, index: true
78
+ t.string :target_model, null: false
79
+ t.string :attr_name, null: false
80
+ t.string :form_element, null: false
81
+ t.string :after_attr
82
+ t.string :before_attr
83
+ t.string :label
84
+ t.string :locale
85
+ t.integer :position
86
+ t.string :migration_version
87
+ t.timestamps
88
+ end
89
+
52
90
  # PaperTrail (paper_trail 17.x shape).
53
91
  create_table :versions, force: true do |t|
54
92
  t.string :item_type, null: false
@@ -75,6 +113,9 @@ class InlineFormsIntegrationTestCase < ActionDispatch::IntegrationTest
75
113
  Kind.delete_all
76
114
  Part.delete_all
77
115
  Machine.delete_all
116
+ Gizmo.delete_all
78
117
  PaperTrail::Version.delete_all
118
+ InlineForms::SchemaIntentRecord.delete_all
119
+ InlineForms::SchemaBatch.delete_all
79
120
  end
80
121
  end
@@ -0,0 +1,73 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+
7
+ class SchemaApplyTest < Minitest::Test
8
+ def intent(**overrides)
9
+ InlineForms::SchemaIntent.new(
10
+ **{ model_name: "Apartment", attribute: :internal_note, form_element: :text_field }.merge(overrides)
11
+ )
12
+ end
13
+
14
+ def test_generator_command
15
+ apply = InlineForms::SchemaApply.new(intent(after: :name))
16
+ assert_equal(
17
+ %w[bundle exec rails g inline_forms_addto Apartment internal_note:text_field --after=name],
18
+ apply.generator_command
19
+ )
20
+ end
21
+
22
+ def test_commands_are_ordered_generate_then_migrate
23
+ apply = InlineForms::SchemaApply.new(intent)
24
+ assert_equal %i[generate migrate], apply.commands.keys
25
+ assert_equal %w[bundle exec rails db:migrate], apply.commands[:migrate]
26
+ end
27
+
28
+ def test_preview_plan_is_human_readable
29
+ plan = InlineForms::SchemaApply.new(intent).preview_plan
30
+ assert_equal 2, plan.size
31
+ assert_match(/\Agenerate: bundle exec rails g inline_forms_addto Apartment/, plan.first)
32
+ end
33
+
34
+ def test_run_bang_executes_in_order_with_injected_runner
35
+ calls = []
36
+ runner = ->(cmd) { calls << cmd; true }
37
+ result = InlineForms::SchemaApply.new(intent, runner: runner).run!
38
+ assert_nil result, "expected success (nil)"
39
+ assert_equal 2, calls.size
40
+ assert_equal %w[bundle exec rails g inline_forms_addto Apartment internal_note:text_field], calls.first
41
+ assert_equal %w[bundle exec rails db:migrate], calls.last
42
+ end
43
+
44
+ def test_generate_returns_only_a_newly_created_migration
45
+ Dir.mktmpdir("schema_apply_generate") do |root|
46
+ migrate_dir = File.join(root, "db", "migrate")
47
+ FileUtils.mkdir_p(migrate_dir)
48
+ # A pre-existing addto migration must NOT be reported when this run
49
+ # creates nothing (the :header case).
50
+ FileUtils.touch(File.join(migrate_dir, "20200101000000_inline_forms_add_to_widgets_old.rb"))
51
+
52
+ header_executor = ->(_args, _root) { } # header: no migration created
53
+ result = InlineForms::SchemaApply.new(intent(form_element: :header))
54
+ .generate!(destination_root: root, executor: header_executor)
55
+ assert_nil result, "header apply must report no migration, not a stale one"
56
+
57
+ # A run that creates a migration reports that one.
58
+ creating = lambda do |_args, _root|
59
+ FileUtils.touch(File.join(migrate_dir, "20260101000000_inline_forms_add_to_widgets_note.rb"))
60
+ end
61
+ created = InlineForms::SchemaApply.new(intent).generate!(destination_root: root, executor: creating)
62
+ assert_equal "20260101000000_inline_forms_add_to_widgets_note.rb", File.basename(created)
63
+ end
64
+ end
65
+
66
+ def test_run_bang_stops_at_first_failure
67
+ calls = []
68
+ runner = ->(cmd) { calls << cmd; false } # generate fails
69
+ result = InlineForms::SchemaApply.new(intent, runner: runner).run!
70
+ assert_equal :generate, result
71
+ assert_equal 1, calls.size, "must not run migrate after generate failed"
72
+ end
73
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class SchemaIntentTest < Minitest::Test
6
+ def intent(**overrides)
7
+ InlineForms::SchemaIntent.new(
8
+ **{ model_name: "Apartment", attribute: :internal_note, form_element: :text_field }.merge(overrides)
9
+ )
10
+ end
11
+
12
+ def test_normalizes_types
13
+ i = intent(attribute: "note", form_element: "text_field", after: "name")
14
+ assert_equal "Apartment", i.model_name
15
+ assert_equal :note, i.attribute
16
+ assert_equal :text_field, i.form_element
17
+ assert_equal :name, i.after
18
+ assert_equal :draft, i.status
19
+ end
20
+
21
+ def test_column_token
22
+ assert_equal "internal_note:text_field", intent.column_token
23
+ end
24
+
25
+ def test_generator_args_plain
26
+ assert_equal [ "Apartment", "internal_note:text_field" ], intent.generator_args
27
+ end
28
+
29
+ def test_generator_args_with_after
30
+ assert_equal [ "Apartment", "internal_note:text_field", "--after=name" ],
31
+ intent(after: :name).generator_args
32
+ end
33
+
34
+ def test_generator_args_with_before
35
+ assert_equal [ "Apartment", "internal_note:text_field", "--before=created_at" ],
36
+ intent(before: :created_at).generator_args
37
+ end
38
+
39
+ def test_status_validation
40
+ assert_raises(ArgumentError) { intent(status: :bogus) }
41
+ i = intent
42
+ i.status = :applied
43
+ assert_equal :applied, i.status
44
+ end
45
+
46
+ def test_to_h_round_trips_core_fields
47
+ h = intent(after: :name, values: { 1 => "a" }).to_h
48
+ assert_equal "Apartment", h[:model_name]
49
+ assert_equal :internal_note, h[:attribute]
50
+ assert_equal({ 1 => "a" }, h[:values])
51
+ assert_equal :name, h[:after]
52
+ end
53
+ end
@@ -0,0 +1,61 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+ require "tmpdir"
5
+ require "fileutils"
6
+
7
+ class SchemaLabelTest < Minitest::Test
8
+ # Minimal stand-in for an AR model name (avoids booting ActiveRecord).
9
+ ModelName = Struct.new(:i18n_key)
10
+ class FakeModel
11
+ def self.model_name = ModelName.new(:apartment)
12
+ end
13
+
14
+ def setup
15
+ @root = Dir.mktmpdir("schema_label_test")
16
+ FileUtils.mkdir_p(File.join(@root, "config", "locales"))
17
+ end
18
+
19
+ def teardown
20
+ FileUtils.remove_entry(@root) if @root && Dir.exist?(@root)
21
+ end
22
+
23
+ def read_yaml(path)
24
+ YAML.safe_load(File.read(path))
25
+ end
26
+
27
+ def test_writes_label_under_the_human_attribute_name_key
28
+ path = InlineForms::SchemaLabel.write(
29
+ destination_root: @root, model_class: FakeModel,
30
+ attribute: :internal_note, label: "Internal note", locale: :en
31
+ )
32
+ assert_equal File.join(@root, "config/locales/inline_forms_labels.en.yml"), path
33
+ doc = read_yaml(path)
34
+ assert_equal "Internal note",
35
+ doc.dig("en", "activerecord", "attributes", "apartment", "internal_note")
36
+ end
37
+
38
+ def test_merges_without_clobbering_existing_labels
39
+ InlineForms::SchemaLabel.write(destination_root: @root, model_class: FakeModel,
40
+ attribute: :first_field, label: "First", locale: :en)
41
+ InlineForms::SchemaLabel.write(destination_root: @root, model_class: FakeModel,
42
+ attribute: :second_field, label: "Second", locale: :en)
43
+
44
+ attrs = read_yaml(InlineForms::SchemaLabel.file_path(@root, :en))
45
+ .dig("en", "activerecord", "attributes", "apartment")
46
+ assert_equal "First", attrs["first_field"]
47
+ assert_equal "Second", attrs["second_field"]
48
+ end
49
+
50
+ def test_separate_file_per_locale
51
+ InlineForms::SchemaLabel.write(destination_root: @root, model_class: FakeModel,
52
+ attribute: :note, label: "Note", locale: :en)
53
+ InlineForms::SchemaLabel.write(destination_root: @root, model_class: FakeModel,
54
+ attribute: :note, label: "Notitie", locale: :nl)
55
+
56
+ assert_equal "Note", read_yaml(InlineForms::SchemaLabel.file_path(@root, :en))
57
+ .dig("en", "activerecord", "attributes", "apartment", "note")
58
+ assert_equal "Notitie", read_yaml(InlineForms::SchemaLabel.file_path(@root, :nl))
59
+ .dig("nl", "activerecord", "attributes", "apartment", "note")
60
+ end
61
+ end
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.40
4
+ version: 8.1.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -102,6 +102,7 @@ files:
102
102
  - ".rubocop_todo.yml"
103
103
  - ".ruby-version"
104
104
  - CHANGELOG.md
105
+ - CLAUDE.md
105
106
  - Gemfile
106
107
  - LICENSE.txt
107
108
  - README.rdoc
@@ -150,11 +151,13 @@ files:
150
151
  - app/views/inline_forms/_list.html.erb
151
152
  - app/views/inline_forms/_new.html.erb
152
153
  - app/views/inline_forms/_new_nested.html.erb
154
+ - app/views/inline_forms/_pending_migration_row.html.erb
153
155
  - app/views/inline_forms/_show.html.erb
154
156
  - app/views/inline_forms/_versions.html.erb
155
157
  - app/views/inline_forms/_versions_list.html.erb
156
158
  - app/views/inline_forms/create_list_frame.html.erb
157
159
  - app/views/inline_forms/field_edit.html.erb
160
+ - app/views/inline_forms/field_pending.html.erb
158
161
  - app/views/inline_forms/field_show.html.erb
159
162
  - app/views/inline_forms/new_record.html.erb
160
163
  - app/views/inline_forms/row_close.html.erb
@@ -206,6 +209,7 @@ files:
206
209
  - lib/generators/templates/test.erb
207
210
  - lib/inline_forms.rb
208
211
  - lib/inline_forms/archived_form_elements.rb
212
+ - lib/inline_forms/attribute_list.rb
209
213
  - lib/inline_forms/form_element_from_callee.rb
210
214
  - lib/inline_forms/form_element_registry.rb
211
215
  - lib/inline_forms/form_elements.rb
@@ -245,6 +249,10 @@ files:
245
249
  - lib/inline_forms/form_elements/text_field_helper.rb
246
250
  - lib/inline_forms/form_elements/time_helper.rb
247
251
  - lib/inline_forms/gem_files.rb
252
+ - lib/inline_forms/schema_apply.rb
253
+ - lib/inline_forms/schema_intent.rb
254
+ - lib/inline_forms/schema_label.rb
255
+ - lib/inline_forms/schema_preview.rb
248
256
  - lib/inline_forms/tabs.rb
249
257
  - lib/inline_forms/turbo_tabs_builder.rb
250
258
  - lib/inline_forms/version.rb
@@ -256,12 +264,15 @@ files:
256
264
  - lib/vagrant/vagrantbox-inline_forms.zip
257
265
  - script/migrate_form_elements.rb
258
266
  - test/archived_form_elements_test.rb
267
+ - test/attribute_list_test.rb
259
268
  - test/dummy/app/controllers/application_controller.rb
269
+ - test/dummy/app/controllers/gizmos_controller.rb
260
270
  - test/dummy/app/controllers/machines_controller.rb
261
271
  - test/dummy/app/controllers/parts_controller.rb
262
272
  - test/dummy/app/controllers/widgets_controller.rb
263
273
  - test/dummy/app/helpers/application_helper.rb
264
274
  - test/dummy/app/models/application_record.rb
275
+ - test/dummy/app/models/gizmo.rb
265
276
  - test/dummy/app/models/kind.rb
266
277
  - test/dummy/app/models/machine.rb
267
278
  - test/dummy/app/models/part.rb
@@ -281,8 +292,15 @@ files:
281
292
  - test/integration/legacy_elements_test.rb
282
293
  - test/integration/native_inputs_test.rb
283
294
  - test/integration/open_after_create_test.rb
295
+ - test/integration/pending_migration_gate_test.rb
296
+ - test/integration/schema_batch_pipeline_test.rb
297
+ - test/integration/schema_edit_test.rb
298
+ - test/integration/schema_preview_test.rb
284
299
  - test/integration_test_helper.rb
285
300
  - test/plain_text_configuration_test.rb
301
+ - test/schema_apply_test.rb
302
+ - test/schema_intent_test.rb
303
+ - test/schema_label_test.rb
286
304
  - test/tabs_test.rb
287
305
  - test/test_helper.rb
288
306
  - test/user_model_config_test.rb
@@ -314,12 +332,15 @@ specification_version: 4
314
332
  summary: Inline editing of forms for Rails 8.
315
333
  test_files:
316
334
  - test/archived_form_elements_test.rb
335
+ - test/attribute_list_test.rb
317
336
  - test/dummy/app/controllers/application_controller.rb
337
+ - test/dummy/app/controllers/gizmos_controller.rb
318
338
  - test/dummy/app/controllers/machines_controller.rb
319
339
  - test/dummy/app/controllers/parts_controller.rb
320
340
  - test/dummy/app/controllers/widgets_controller.rb
321
341
  - test/dummy/app/helpers/application_helper.rb
322
342
  - test/dummy/app/models/application_record.rb
343
+ - test/dummy/app/models/gizmo.rb
323
344
  - test/dummy/app/models/kind.rb
324
345
  - test/dummy/app/models/machine.rb
325
346
  - test/dummy/app/models/part.rb
@@ -339,8 +360,15 @@ test_files:
339
360
  - test/integration/legacy_elements_test.rb
340
361
  - test/integration/native_inputs_test.rb
341
362
  - test/integration/open_after_create_test.rb
363
+ - test/integration/pending_migration_gate_test.rb
364
+ - test/integration/schema_batch_pipeline_test.rb
365
+ - test/integration/schema_edit_test.rb
366
+ - test/integration/schema_preview_test.rb
342
367
  - test/integration_test_helper.rb
343
368
  - test/plain_text_configuration_test.rb
369
+ - test/schema_apply_test.rb
370
+ - test/schema_intent_test.rb
371
+ - test/schema_label_test.rb
344
372
  - test/tabs_test.rb
345
373
  - test/test_helper.rb
346
374
  - test/user_model_config_test.rb