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
+ # -*- encoding : utf-8 -*-
2
+
3
+ require "yaml"
4
+ require "fileutils"
5
+
6
+ module InlineForms
7
+ # Writes a human label for a model attribute (or header) into a locale file,
8
+ # so `human_attribute_name` renders it. Used by the schema GUI's optional
9
+ # "Label" field. Like the header/field edits, this is a code/config change
10
+ # that lands in the working tree (git-tracked) — never the DB.
11
+ #
12
+ # The label is merged (not clobbered) into
13
+ # config/locales/inline_forms_labels.<locale>.yml
14
+ # under the key inline_forms renders from:
15
+ # <locale>: activerecord: attributes: <model_i18n_key>: <attribute>: "Label"
16
+ #
17
+ # A dedicated file (rather than editing an existing app locale file) keeps the
18
+ # write a whole-document load -> deep_merge -> dump, which is robust against
19
+ # comments/formatting concerns.
20
+ module SchemaLabel
21
+ module_function
22
+
23
+ def file_path(destination_root, locale)
24
+ File.join(destination_root.to_s, "config", "locales", "inline_forms_labels.#{locale}.yml")
25
+ end
26
+
27
+ def i18n_key_for(model_class)
28
+ if model_class.respond_to?(:model_name)
29
+ model_class.model_name.i18n_key.to_s
30
+ else
31
+ model_class.to_s.underscore
32
+ end
33
+ end
34
+
35
+ # Merge the label into the locale file and return its path.
36
+ def write(destination_root:, model_class:, attribute:, label:, locale:)
37
+ path = file_path(destination_root, locale)
38
+ key = i18n_key_for(model_class)
39
+ existing = load_document(path)
40
+ addition = {
41
+ locale.to_s => {
42
+ "activerecord" => {
43
+ "attributes" => {
44
+ key => { attribute.to_s => label.to_s }
45
+ }
46
+ }
47
+ }
48
+ }
49
+ merged = existing.deep_merge(addition)
50
+
51
+ FileUtils.mkdir_p(File.dirname(path))
52
+ File.write(path, merged.to_yaml)
53
+ path
54
+ end
55
+
56
+ def load_document(path)
57
+ return {} unless File.exist?(path)
58
+
59
+ loaded = YAML.safe_load(File.read(path))
60
+ loaded.is_a?(Hash) ? loaded : {}
61
+ rescue StandardError
62
+ {}
63
+ end
64
+ end
65
+ end
@@ -0,0 +1,135 @@
1
+ # -*- encoding : utf-8 -*-
2
+
3
+ module InlineForms
4
+ # Cheap preview of a SchemaIntent, WITHOUT running a migration or booting a
5
+ # second app. The trick (see the staging doc): declare the proposed attribute
6
+ # as a *virtual* typed attribute on a throwaway subclass of the target model,
7
+ # so it reads/writes in memory with no column, and render the stock inline
8
+ # forms against an AttributeList that has the proposed row spliced in at the
9
+ # requested position. The real class and its table are untouched.
10
+ #
11
+ # obj, list = InlineForms::SchemaPreview.build(intent, existing_record)
12
+ # # render "inline_forms/show" with @object = obj,
13
+ # # @inline_forms_attribute_list = list (the controller already supports
14
+ # # this per-request override — see attr_writer :inline_forms_attribute_list)
15
+ #
16
+ # Scope of the stab: scalar attributes preview fully. Relation dropdowns
17
+ # (backed by a foreign key + association) and uploader/rich_text fields need
18
+ # model-side wiring the preview does not synthesize; #supported? reports this
19
+ # so a caller can fall back to a "available after apply" placeholder.
20
+ module SchemaPreview
21
+ module_function
22
+
23
+ # Map an AR column type (from the engine's form-element maps) to an
24
+ # ActiveModel::Type symbol for a virtual attribute.
25
+ COLUMN_TYPE_TO_VIRTUAL = {
26
+ string: :string,
27
+ text: :string,
28
+ integer: :integer,
29
+ decimal: :decimal,
30
+ float: :float,
31
+ boolean: :boolean,
32
+ date: :date,
33
+ time: :time,
34
+ datetime: :datetime,
35
+ timestamp: :datetime
36
+ }.freeze
37
+
38
+ # Form elements the GUI must not offer as "add a scalar field":
39
+ # * :header / :info are not real fields — :header is a display-only
40
+ # separator (adding one via addto would create a pointless string
41
+ # column), :info is read-only over an existing column.
42
+ # * uploaders need a mounted CarrierWave uploader; :money_field a
43
+ # monetize declaration; :devise_password_field a Devise model; :pdf_link
44
+ # a route — a virtual attribute alone cannot faithfully preview them.
45
+ PREVIEW_UNSUPPORTED_FORM_ELEMENTS = %i[
46
+ header
47
+ info
48
+ image_field
49
+ audio_field
50
+ file_field
51
+ multi_image_field
52
+ simple_file_field
53
+ pdf_link
54
+ money_field
55
+ devise_password_field
56
+ ].freeze
57
+
58
+ # Returns [preview_object, attribute_list]. `base_record` seeds the preview
59
+ # object's existing attributes when given (so the rest of the form shows
60
+ # real data); otherwise a blank instance is used.
61
+ def build(intent, base_record = nil)
62
+ base_class = intent.model_class
63
+ list = attribute_list_for(base_class, intent)
64
+ preview = preview_class_for(base_class, intent)
65
+
66
+ object = base_record ? preview.new(base_record.attributes) : preview.new
67
+ object.inline_forms_attribute_list = list
68
+ [ object, list ]
69
+ end
70
+
71
+ # False when the intent's form element needs model-side wiring the preview
72
+ # cannot synthesize (relations, uploaders, rich_text).
73
+ def supported?(intent)
74
+ !virtual_type(intent.form_element).nil?
75
+ end
76
+
77
+ # The sorted list of form elements the GUI offers (those that preview via a
78
+ # virtual attribute): scalar text/number/date/boolean/choice elements.
79
+ def supported_form_elements
80
+ candidates = InlineForms::SPECIAL_COLUMN_TYPES.keys +
81
+ InlineForms::DEFAULT_FORM_ELEMENTS.values
82
+ candidates.uniq.select { |fe| !virtual_type(fe).nil? }.sort
83
+ end
84
+
85
+ # The AttributeList the preview should render: the model's current list with
86
+ # the proposed row inserted at --after/--before (falling back to append).
87
+ def attribute_list_for(base_class, intent)
88
+ current = base_class.new.inline_forms_attribute_list
89
+ list = InlineForms::AttributeList.wrap(current.map(&:dup))
90
+ opts = { values: intent.values, disabled: intent.disabled }.compact
91
+
92
+ # Value-bearing choice elements need a values hash or their _edit/_show
93
+ # helper raises. Mirror the addto generator: inject a placeholder the user
94
+ # edits after apply (so preview and apply produce the same row).
95
+ if opts[:values].nil? && InlineForms::VALUE_BEARING_FORM_ELEMENTS.include?(intent.form_element)
96
+ opts[:values] = InlineForms::PLACEHOLDER_VALUES
97
+ end
98
+
99
+ if intent.after && list.include_attribute?(intent.after)
100
+ list.insert_after(intent.after, intent.attribute, intent.form_element, **opts)
101
+ elsif intent.before && list.include_attribute?(intent.before)
102
+ list.insert_before(intent.before, intent.attribute, intent.form_element, **opts)
103
+ else
104
+ list.field(intent.attribute, intent.form_element, **opts)
105
+ end
106
+ list
107
+ end
108
+
109
+ # A throwaway subclass with the proposed attribute declared virtual (when
110
+ # supported), masquerading as the base class so partials/routes resolve.
111
+ def preview_class_for(base_class, intent)
112
+ type = virtual_type(intent.form_element)
113
+ klass = Class.new(base_class)
114
+ klass.attribute(intent.attribute, type) if type
115
+
116
+ base_name = base_class.name
117
+ base_model_name = base_class.model_name
118
+ klass.define_singleton_method(:name) { base_name }
119
+ klass.define_singleton_method(:model_name) { base_model_name }
120
+ klass
121
+ end
122
+
123
+ def virtual_type(form_element)
124
+ fe = form_element.to_sym
125
+ return nil if PREVIEW_UNSUPPORTED_FORM_ELEMENTS.include?(fe)
126
+
127
+ column_type =
128
+ InlineForms::SPECIAL_COLUMN_TYPES[fe] ||
129
+ (InlineForms::DEFAULT_FORM_ELEMENTS.value?(fe) ? :string : nil)
130
+ return nil if column_type.nil? || column_type == :no_migration || column_type == :belongs_to
131
+
132
+ COLUMN_TYPE_TO_VIRTUAL[column_type]
133
+ end
134
+ end
135
+ end
@@ -1,5 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  module InlineForms
4
- VERSION = "8.1.40"
4
+ VERSION = "8.1.42"
5
5
  end
data/lib/inline_forms.rb CHANGED
@@ -1,6 +1,11 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
 
3
3
  require "inline_forms/version"
4
+ require "inline_forms/attribute_list"
5
+ require "inline_forms/schema_intent"
6
+ require "inline_forms/schema_preview"
7
+ require "inline_forms/schema_apply"
8
+ require "inline_forms/schema_label"
4
9
  require "inline_forms/form_element_from_callee"
5
10
  require "inline_forms/archived_form_elements"
6
11
  require "inline_forms/form_element_registry"
@@ -106,6 +111,25 @@ module InlineForms
106
111
  plain_text_area
107
112
  ].freeze
108
113
 
114
+ # Form elements that render a set of choices and therefore REQUIRE a values
115
+ # hash as the 3rd element of their attribute_list row (their _show/_edit
116
+ # helpers call `attribute_values`, which raises when it is missing). Shared by
117
+ # the addto generator and the schema GUI/preview so both insert a placeholder.
118
+ VALUE_BEARING_FORM_ELEMENTS = %i[
119
+ dropdown_with_values
120
+ dropdown_with_integers
121
+ dropdown_with_values_with_stars
122
+ radio_button
123
+ check_box
124
+ scale_with_integers
125
+ scale_with_values
126
+ slider_with_values
127
+ ].freeze
128
+
129
+ # Placeholder values hash inserted when a value-bearing element is added
130
+ # programmatically (generator / GUI); the user edits it to real values after.
131
+ PLACEHOLDER_VALUES = { 1 => "one", 2 => "two" }.freeze
132
+
109
133
  def self.plain_text_form_element?(form_element)
110
134
  PLAIN_TEXT_FORM_ELEMENTS.include?(form_element.to_sym)
111
135
  rescue NoMethodError
@@ -121,6 +145,68 @@ module InlineForms
121
145
  "Use :rich_text for ActionText-backed attributes, or add a text column for :plain_text."
122
146
  end
123
147
 
148
+ # Form elements exempt from the pending-migration gate. They either render a
149
+ # label only (:header), or read a *virtual* attribute whose name is not its
150
+ # backing column — :devise_password_field (backed by encrypted_password),
151
+ # :money_field (backed by a *_cents column via money-rails), :info
152
+ # (conventionally bound to pre-existing columns like created_at). None of
153
+ # these are produced by `inline_forms_addto` in the transient pre-migrate
154
+ # window, so gating them would only ever mis-fire.
155
+ PENDING_GATE_EXEMPT_FORM_ELEMENTS = %i[
156
+ header
157
+ info
158
+ devise_password_field
159
+ money_field
160
+ ].freeze
161
+
162
+ # True when an attribute_list row references a column that its model's table
163
+ # does not have yet — i.e. the model file was edited (row added) but the
164
+ # migration adding the column has not run. Rendering or writing such a row
165
+ # raises (e.g. `object[:foo]` / `foo_show` on a missing column), so callers
166
+ # gate on this to show a "pending migration" placeholder and skip writes
167
+ # instead of 500ing during the window between `rails g inline_forms_addto`
168
+ # and `rails db:migrate`.
169
+ #
170
+ # Detection is column-presence based (robust): the expected column is derived
171
+ # from the form element via the same type maps the generator uses. `nil`
172
+ # (associations, rich_text, pdf_link, unknown elements) and virtual-backed
173
+ # elements (see PENDING_GATE_EXEMPT_FORM_ELEMENTS) are never gated. The gate
174
+ # self-heals: once the migration runs and the schema cache reflects the new
175
+ # column, the column is present and the row renders normally — no flag to
176
+ # clear, nothing to maintain.
177
+ def self.attribute_pending_migration?(object, attribute, form_element)
178
+ fe = form_element.to_sym
179
+ return false if PENDING_GATE_EXEMPT_FORM_ELEMENTS.include?(fe)
180
+
181
+ klass = object.class
182
+ return false unless klass.respond_to?(:table_exists?) && klass.respond_to?(:column_names)
183
+ return false unless klass.table_exists?
184
+
185
+ column = pending_gate_expected_column(attribute, fe)
186
+ return false if column.nil?
187
+
188
+ !klass.column_names.include?(column)
189
+ rescue StandardError
190
+ # The gate must never itself break rendering; fail open (render as before).
191
+ false
192
+ end
193
+
194
+ # The column an attribute_list row expects on the model's own table, or nil
195
+ # when the form element needs no such column (has_many/habtm/has_one/
196
+ # rich_text -> :no_migration; pdf_link/info_list/unknown -> nil). Relation
197
+ # dropdowns are backed by a foreign key (`<attribute>_id`); every other
198
+ # column-backed element uses a column named after the attribute.
199
+ def self.pending_gate_expected_column(attribute, form_element)
200
+ column_type =
201
+ SPECIAL_COLUMN_TYPES[form_element] ||
202
+ (DEFAULT_FORM_ELEMENTS.value?(form_element) ? :__scalar__ : nil)
203
+
204
+ return nil if column_type.nil? || column_type == :no_migration
205
+ return "#{attribute}_id" if column_type == :belongs_to
206
+
207
+ attribute.to_s
208
+ end
209
+
124
210
  def self.validate_plain_text_configuration_for!(klass)
125
211
  return unless klass.respond_to?(:table_exists?) &&
126
212
  klass.respond_to?(:column_names) &&
@@ -31,6 +31,8 @@ en:
31
31
  trash: trash
32
32
  list_versions: List versions
33
33
  close_versions_list: Close versions list
34
+ pending_migration:
35
+ notice: "pending migration \u2014 run rails db:migrate"
34
36
  succes: success.
35
37
  activerecord:
36
38
  models:
@@ -28,6 +28,8 @@ nl:
28
28
  add_new: Maak een nieuw %{model}
29
29
  undo: terughalen
30
30
  trash: weggooien
31
+ pending_migration:
32
+ notice: "migratie in afwachting \u2014 draai rails db:migrate"
31
33
  succes: het ding is aangemaakt.
32
34
  activerecord:
33
35
  models:
@@ -0,0 +1,165 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "test_helper"
4
+
5
+ class AttributeListTest < Minitest::Test
6
+ def build_sample
7
+ InlineForms::AttributeList.build do
8
+ header :basics
9
+ field :name, :text_field
10
+ field :priority, :dropdown_with_values, values: { 1 => "low", 2 => "high" }
11
+ field :flag, :dropdown_with_values, values: { 1 => "a", 2 => "b" }, disabled: [ 2 ]
12
+ info :created_at, :updated_at
13
+ end
14
+ end
15
+
16
+ # -- array compatibility (the whole point) -----------------------------
17
+
18
+ def test_is_an_array
19
+ assert_kind_of Array, build_sample
20
+ end
21
+
22
+ def test_destructures_like_the_legacy_literal
23
+ seen = []
24
+ build_sample.each { |attribute, form_element| seen << [ attribute, form_element ] }
25
+ assert_includes seen, [ :name, :text_field ]
26
+ assert_includes seen, [ :priority, :dropdown_with_values ]
27
+ end
28
+
29
+ def test_assoc_positional_values_hash_still_works
30
+ list = build_sample
31
+ # Consumers fetch the values hash as list.assoc(attr)[2].
32
+ assert_equal({ 1 => "low", 2 => "high" }, list.assoc(:priority)[2])
33
+ assert_equal([ 2 ], list.assoc(:flag)[3])
34
+ assert_nil list.assoc(:name)[2]
35
+ end
36
+
37
+ def test_build_produces_expected_rows
38
+ list = build_sample
39
+ assert_equal(
40
+ [
41
+ [ :basics, :header ],
42
+ [ :name, :text_field ],
43
+ [ :priority, :dropdown_with_values, { 1 => "low", 2 => "high" } ],
44
+ [ :flag, :dropdown_with_values, { 1 => "a", 2 => "b" }, [ 2 ] ],
45
+ [ :created_at, :info ],
46
+ [ :updated_at, :info ]
47
+ ],
48
+ list.to_a
49
+ )
50
+ end
51
+
52
+ # -- append -------------------------------------------------------------
53
+
54
+ def test_field_appends_and_returns_self_for_chaining
55
+ list = InlineForms::AttributeList.new
56
+ result = list.field(:a, :text_field).field(:b, :check_box)
57
+ assert_same list, result
58
+ assert_equal %i[a b], list.names
59
+ end
60
+
61
+ def test_disabled_without_values_raises
62
+ assert_raises(ArgumentError) do
63
+ InlineForms::AttributeList.new.field(:x, :dropdown_with_values, disabled: [ 1 ])
64
+ end
65
+ end
66
+
67
+ # -- insertion ----------------------------------------------------------
68
+
69
+ def test_insert_after
70
+ list = build_sample.insert_after(:name, :nickname, :text_field)
71
+ assert_equal :nickname, list.names[list.index_of(:name) + 1]
72
+ end
73
+
74
+ def test_insert_before
75
+ list = build_sample.insert_before(:priority, :urgent, :check_box)
76
+ assert_equal :urgent, list.names[list.index_of(:priority) - 1]
77
+ end
78
+
79
+ def test_insert_after_missing_anchor_raises
80
+ assert_raises(ArgumentError) { build_sample.insert_after(:nope, :x, :text_field) }
81
+ end
82
+
83
+ # -- removal ------------------------------------------------------------
84
+
85
+ def test_remove_present
86
+ list = build_sample.remove(:priority)
87
+ refute list.include_attribute?(:priority)
88
+ end
89
+
90
+ def test_remove_absent_is_noop
91
+ list = build_sample
92
+ before = list.dup
93
+ list.remove(:does_not_exist)
94
+ assert_equal before.to_a, list.to_a
95
+ end
96
+
97
+ # -- reordering ---------------------------------------------------------
98
+
99
+ def test_move_after
100
+ list = build_sample.move_after(:name, :updated_at)
101
+ assert_equal :name, list.names.last
102
+ assert_equal 1, list.names.count(:name)
103
+ end
104
+
105
+ def test_move_before
106
+ list = build_sample.move_before(:updated_at, :basics)
107
+ assert_equal 0, list.index_of(:updated_at)
108
+ assert_equal 1, list.names.count(:updated_at)
109
+ end
110
+
111
+ def test_move_relative_to_itself_raises
112
+ assert_raises(ArgumentError) { build_sample.move_after(:name, :name) }
113
+ end
114
+
115
+ # -- replacement --------------------------------------------------------
116
+
117
+ def test_replace_element_preserves_position
118
+ list = build_sample
119
+ pos = list.index_of(:priority)
120
+ list.replace_element(:priority, :radio_button, values: { 1 => "x" })
121
+ assert_equal pos, list.index_of(:priority)
122
+ assert_equal :radio_button, list.form_element_for(:priority)
123
+ assert_equal({ 1 => "x" }, list.values_for(:priority))
124
+ end
125
+
126
+ def test_replace_element_absent_is_noop
127
+ list = build_sample
128
+ before = list.dup
129
+ list.replace_element(:nope, :text_field)
130
+ assert_equal before.to_a, list.to_a
131
+ end
132
+
133
+ # -- queries ------------------------------------------------------------
134
+
135
+ def test_query_helpers
136
+ list = build_sample
137
+ assert_equal 1, list.index_of(:name)
138
+ assert list.include_attribute?(:flag)
139
+ refute list.include_attribute?(:ghost)
140
+ assert_equal :dropdown_with_values, list.form_element_for(:priority)
141
+ assert_equal({ 1 => "low", 2 => "high" }, list.values_for(:priority))
142
+ assert_equal [ :name, :text_field ], list.row_for(:name)
143
+ end
144
+
145
+ # -- wrap ---------------------------------------------------------------
146
+
147
+ def test_wrap_existing_array_enables_editing
148
+ legacy = [ [ :name, :text_field ], [ :email, :text_field ] ]
149
+ list = InlineForms::AttributeList.wrap(legacy)
150
+ assert_kind_of InlineForms::AttributeList, list
151
+ list.insert_after(:name, :phone, :text_field)
152
+ assert_equal %i[name phone email], list.names
153
+ end
154
+
155
+ def test_wrap_is_idempotent_for_attribute_list
156
+ list = build_sample
157
+ assert_same list, InlineForms::AttributeList.wrap(list)
158
+ end
159
+
160
+ def test_module_level_builder
161
+ list = InlineForms.attribute_list { field :a, :text_field }
162
+ assert_kind_of InlineForms::AttributeList, list
163
+ assert_equal %i[a], list.names
164
+ end
165
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ class GizmosController < InlineFormsController
4
+ end
@@ -0,0 +1,25 @@
1
+ # frozen_string_literal: true
2
+
3
+ # Models the `inline_forms_addto` pre-migrate window: the attribute_list has a
4
+ # row (`:pending_note`) whose column does NOT exist on the gizmos table yet, so
5
+ # it exercises InlineForms.attribute_pending_migration? end to end. `:name` and
6
+ # `:created_at` are real columns; `:section` is a header. See
7
+ # test/integration/pending_migration_gate_test.rb.
8
+ class Gizmo < ApplicationRecord
9
+ validates :name, presence: true
10
+
11
+ scope :inline_forms_list, -> { order(:name, :id) }
12
+
13
+ def _presentation
14
+ "#{name}"
15
+ end
16
+
17
+ def inline_forms_attribute_list
18
+ @inline_forms_attribute_list ||= [
19
+ [ :name, :text_field ],
20
+ [ :section, :header ],
21
+ [ :pending_note, :text_field ],
22
+ [ :created_at, :info ]
23
+ ]
24
+ end
25
+ end
@@ -16,6 +16,9 @@ require "action_view/railtie"
16
16
  Bundler.require(*Rails.groups)
17
17
 
18
18
  require "inline_forms"
19
+ # The schema-GUI engine (separate gem, same repo; on the load path via the
20
+ # Gemfile's test group). Its integration tests live in test/integration/.
21
+ require "inline_forms_schema_edit"
19
22
 
20
23
  module Dummy
21
24
  class Application < Rails::Application
@@ -19,4 +19,13 @@ Rails.application.routes.draw do
19
19
  post "revert", on: :member
20
20
  get "list_versions", on: :member
21
21
  end
22
+
23
+ resources :gizmos do
24
+ post "revert", on: :member
25
+ get "list_versions", on: :member
26
+ end
27
+
28
+ # Schema-change GUI (opt-in in real installs; routed here for the engine's
29
+ # fast test suite). One line, so gem upgrades can add routes freely.
30
+ InlineFormsSchemaEdit.draw_routes(self)
22
31
  end