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.
- checksums.yaml +4 -4
- data/.forgejo/workflows/full-gate.yml +4 -2
- data/.gitignore +6 -2
- data/.rubocop.yml +1 -0
- data/CHANGELOG.md +44 -0
- data/CLAUDE.md +68 -0
- data/Gemfile +4 -0
- data/Rakefile +52 -11
- data/app/controllers/inline_forms_controller.rb +15 -0
- data/app/views/inline_forms/_new.html.erb +4 -1
- data/app/views/inline_forms/_pending_migration_row.html.erb +15 -0
- data/app/views/inline_forms/_show.html.erb +4 -1
- data/app/views/inline_forms/field_pending.html.erb +10 -0
- data/lib/generators/inline_forms_addto/USAGE +31 -0
- data/lib/generators/inline_forms_addto_generator.rb +198 -26
- data/lib/inline_forms/attribute_list.rb +191 -0
- data/lib/inline_forms/gem_files.rb +26 -0
- data/lib/inline_forms/schema_apply.rb +85 -0
- data/lib/inline_forms/schema_intent.rb +76 -0
- data/lib/inline_forms/schema_label.rb +65 -0
- data/lib/inline_forms/schema_preview.rb +135 -0
- data/lib/inline_forms/version.rb +1 -1
- data/lib/inline_forms.rb +86 -0
- data/lib/locales/inline_forms.en.yml +2 -0
- data/lib/locales/inline_forms.nl.yml +2 -0
- data/test/attribute_list_test.rb +165 -0
- data/test/dummy/app/controllers/gizmos_controller.rb +4 -0
- data/test/dummy/app/models/gizmo.rb +25 -0
- data/test/dummy/config/application.rb +3 -0
- data/test/dummy/config/routes.rb +9 -0
- data/test/inline_forms_addto_generator_test.rb +220 -0
- data/test/integration/pending_migration_gate_test.rb +75 -0
- data/test/integration/schema_batch_pipeline_test.rb +189 -0
- data/test/integration/schema_edit_test.rb +141 -0
- data/test/integration/schema_preview_test.rb +65 -0
- data/test/integration_test_helper.rb +41 -0
- data/test/schema_apply_test.rb +73 -0
- data/test/schema_intent_test.rb +53 -0
- data/test/schema_label_test.rb +61 -0
- 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
|
data/lib/inline_forms/version.rb
CHANGED
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) &&
|
|
@@ -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,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
|
data/test/dummy/config/routes.rb
CHANGED
|
@@ -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
|