inline_forms 8.1.40 → 8.1.43

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/.claude/settings.local.json +9 -0
  3. data/.forgejo/workflows/full-gate.yml +4 -2
  4. data/.gitignore +6 -2
  5. data/.rubocop.yml +1 -0
  6. data/CHANGELOG.md +60 -0
  7. data/CLAUDE.md +68 -0
  8. data/Gemfile +4 -0
  9. data/Rakefile +52 -11
  10. data/app/assets/stylesheets/inline_forms/inline_forms.scss +19 -0
  11. data/app/controllers/inline_forms_controller.rb +15 -0
  12. data/app/views/inline_forms/_new.html.erb +4 -1
  13. data/app/views/inline_forms/_pending_migration_row.html.erb +15 -0
  14. data/app/views/inline_forms/_show.html.erb +4 -1
  15. data/app/views/inline_forms/field_pending.html.erb +10 -0
  16. data/lib/generators/inline_forms_addto/USAGE +31 -0
  17. data/lib/generators/inline_forms_addto_generator.rb +198 -26
  18. data/lib/inline_forms/attribute_list.rb +191 -0
  19. data/lib/inline_forms/gem_files.rb +26 -0
  20. data/lib/inline_forms/schema_apply.rb +85 -0
  21. data/lib/inline_forms/schema_intent.rb +76 -0
  22. data/lib/inline_forms/schema_label.rb +65 -0
  23. data/lib/inline_forms/schema_preview.rb +135 -0
  24. data/lib/inline_forms/version.rb +1 -1
  25. data/lib/inline_forms.rb +86 -0
  26. data/lib/locales/inline_forms.en.yml +2 -0
  27. data/lib/locales/inline_forms.nl.yml +2 -0
  28. data/test/attribute_list_test.rb +165 -0
  29. data/test/dummy/app/controllers/gizmos_controller.rb +4 -0
  30. data/test/dummy/app/models/gizmo.rb +25 -0
  31. data/test/dummy/config/application.rb +3 -0
  32. data/test/dummy/config/routes.rb +9 -0
  33. data/test/inline_forms_addto_generator_test.rb +220 -0
  34. data/test/integration/pending_migration_gate_test.rb +75 -0
  35. data/test/integration/schema_batch_pipeline_test.rb +189 -0
  36. data/test/integration/schema_edit_test.rb +141 -0
  37. data/test/integration/schema_preview_test.rb +65 -0
  38. data/test/integration_test_helper.rb +41 -0
  39. data/test/schema_apply_test.rb +73 -0
  40. data/test/schema_intent_test.rb +53 -0
  41. data/test/schema_label_test.rb +61 -0
  42. metadata +30 -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.43
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -94,6 +94,7 @@ executables: []
94
94
  extensions: []
95
95
  extra_rdoc_files: []
96
96
  files:
97
+ - ".claude/settings.local.json"
97
98
  - ".document"
98
99
  - ".forgejo/workflows/ci.yml"
99
100
  - ".forgejo/workflows/full-gate.yml"
@@ -102,6 +103,7 @@ files:
102
103
  - ".rubocop_todo.yml"
103
104
  - ".ruby-version"
104
105
  - CHANGELOG.md
106
+ - CLAUDE.md
105
107
  - Gemfile
106
108
  - LICENSE.txt
107
109
  - README.rdoc
@@ -150,11 +152,13 @@ files:
150
152
  - app/views/inline_forms/_list.html.erb
151
153
  - app/views/inline_forms/_new.html.erb
152
154
  - app/views/inline_forms/_new_nested.html.erb
155
+ - app/views/inline_forms/_pending_migration_row.html.erb
153
156
  - app/views/inline_forms/_show.html.erb
154
157
  - app/views/inline_forms/_versions.html.erb
155
158
  - app/views/inline_forms/_versions_list.html.erb
156
159
  - app/views/inline_forms/create_list_frame.html.erb
157
160
  - app/views/inline_forms/field_edit.html.erb
161
+ - app/views/inline_forms/field_pending.html.erb
158
162
  - app/views/inline_forms/field_show.html.erb
159
163
  - app/views/inline_forms/new_record.html.erb
160
164
  - app/views/inline_forms/row_close.html.erb
@@ -206,6 +210,7 @@ files:
206
210
  - lib/generators/templates/test.erb
207
211
  - lib/inline_forms.rb
208
212
  - lib/inline_forms/archived_form_elements.rb
213
+ - lib/inline_forms/attribute_list.rb
209
214
  - lib/inline_forms/form_element_from_callee.rb
210
215
  - lib/inline_forms/form_element_registry.rb
211
216
  - lib/inline_forms/form_elements.rb
@@ -245,6 +250,10 @@ files:
245
250
  - lib/inline_forms/form_elements/text_field_helper.rb
246
251
  - lib/inline_forms/form_elements/time_helper.rb
247
252
  - lib/inline_forms/gem_files.rb
253
+ - lib/inline_forms/schema_apply.rb
254
+ - lib/inline_forms/schema_intent.rb
255
+ - lib/inline_forms/schema_label.rb
256
+ - lib/inline_forms/schema_preview.rb
248
257
  - lib/inline_forms/tabs.rb
249
258
  - lib/inline_forms/turbo_tabs_builder.rb
250
259
  - lib/inline_forms/version.rb
@@ -256,12 +265,15 @@ files:
256
265
  - lib/vagrant/vagrantbox-inline_forms.zip
257
266
  - script/migrate_form_elements.rb
258
267
  - test/archived_form_elements_test.rb
268
+ - test/attribute_list_test.rb
259
269
  - test/dummy/app/controllers/application_controller.rb
270
+ - test/dummy/app/controllers/gizmos_controller.rb
260
271
  - test/dummy/app/controllers/machines_controller.rb
261
272
  - test/dummy/app/controllers/parts_controller.rb
262
273
  - test/dummy/app/controllers/widgets_controller.rb
263
274
  - test/dummy/app/helpers/application_helper.rb
264
275
  - test/dummy/app/models/application_record.rb
276
+ - test/dummy/app/models/gizmo.rb
265
277
  - test/dummy/app/models/kind.rb
266
278
  - test/dummy/app/models/machine.rb
267
279
  - test/dummy/app/models/part.rb
@@ -281,8 +293,15 @@ files:
281
293
  - test/integration/legacy_elements_test.rb
282
294
  - test/integration/native_inputs_test.rb
283
295
  - test/integration/open_after_create_test.rb
296
+ - test/integration/pending_migration_gate_test.rb
297
+ - test/integration/schema_batch_pipeline_test.rb
298
+ - test/integration/schema_edit_test.rb
299
+ - test/integration/schema_preview_test.rb
284
300
  - test/integration_test_helper.rb
285
301
  - test/plain_text_configuration_test.rb
302
+ - test/schema_apply_test.rb
303
+ - test/schema_intent_test.rb
304
+ - test/schema_label_test.rb
286
305
  - test/tabs_test.rb
287
306
  - test/test_helper.rb
288
307
  - test/user_model_config_test.rb
@@ -314,12 +333,15 @@ specification_version: 4
314
333
  summary: Inline editing of forms for Rails 8.
315
334
  test_files:
316
335
  - test/archived_form_elements_test.rb
336
+ - test/attribute_list_test.rb
317
337
  - test/dummy/app/controllers/application_controller.rb
338
+ - test/dummy/app/controllers/gizmos_controller.rb
318
339
  - test/dummy/app/controllers/machines_controller.rb
319
340
  - test/dummy/app/controllers/parts_controller.rb
320
341
  - test/dummy/app/controllers/widgets_controller.rb
321
342
  - test/dummy/app/helpers/application_helper.rb
322
343
  - test/dummy/app/models/application_record.rb
344
+ - test/dummy/app/models/gizmo.rb
323
345
  - test/dummy/app/models/kind.rb
324
346
  - test/dummy/app/models/machine.rb
325
347
  - test/dummy/app/models/part.rb
@@ -339,8 +361,15 @@ test_files:
339
361
  - test/integration/legacy_elements_test.rb
340
362
  - test/integration/native_inputs_test.rb
341
363
  - test/integration/open_after_create_test.rb
364
+ - test/integration/pending_migration_gate_test.rb
365
+ - test/integration/schema_batch_pipeline_test.rb
366
+ - test/integration/schema_edit_test.rb
367
+ - test/integration/schema_preview_test.rb
342
368
  - test/integration_test_helper.rb
343
369
  - test/plain_text_configuration_test.rb
370
+ - test/schema_apply_test.rb
371
+ - test/schema_intent_test.rb
372
+ - test/schema_label_test.rb
344
373
  - test/tabs_test.rb
345
374
  - test/test_helper.rb
346
375
  - test/user_model_config_test.rb