inline_forms_installer 8.1.5 → 8.1.8

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7ebb695af4c902a20604e51deb4fc8a2eefaf32e625cf86805633ffd65e048d4
4
- data.tar.gz: 1b630b3fc3f578fbde245ec2957c9cedc60c0dc27cf28d506b56a41361fb9d5c
3
+ metadata.gz: 1db2a592daf12e0ddf1b93c67469f400540e58a81b8ca96ee516dccb7a03dc20
4
+ data.tar.gz: 4e5abd3355753c8f09d79e16431007a0b268066e1106330c4251649262f92fe2
5
5
  SHA512:
6
- metadata.gz: 50c22db56f53d41b37f9aaffb44298478a707fbd8f36d962a280e8d952f1c77a25306f6c9e2ecd10f59b825807ced7f172c6dfa547d613efa3569cee6c3e88ec
7
- data.tar.gz: 1c4b64a00674ca2096c1b40c2f48f7bed3c2bd696f0fdf0cd295c6f7df093b244047040e2b07059c42364dd26e842040218973ea002324aafc6780a388ed7d62
6
+ metadata.gz: 92c4c50ca28769e76767bcc11fc74c742520386cddf55c4559133b95251161dd71d28e2a9e8d3fbddde62b95d9adf6e72f5666b7893ae9f94374132d66af37b6
7
+ data.tar.gz: d71c002620d3959812057383f6714eb01f9863370351d4436cf419e4b7a2ece6f78a2848ca057209fe6b68956fe42e04bdcd1f75731d6880c06d06b960f358b2
@@ -105,6 +105,13 @@ gem 'jquery-ui-rails', '4.0.3'
105
105
  # Foundation Icons SCSS + fonts are vendored in the inline_forms engine (Dart Sass;
106
106
  # foundation-icons-sass-rails depended on sass-rails).
107
107
  gem 'mini_magick'
108
+ # money-rails powers the `money_field` Tier 1 helper:
109
+ # * adds the `humanized_money_with_symbol` view helper used by
110
+ # money_field_show
111
+ # * exposes `monetize :foo_cents` so an integer `_cents` column is
112
+ # read/written as a Money instance through `obj.foo`
113
+ # Used by FormElementShowcase#amount in the --example app.
114
+ gem 'money-rails', '~> 3.0'
108
115
  gem 'mysql2'
109
116
  gem 'paper_trail', '~> 17.0'
110
117
  gem 'rails-i18n', '~> 8.1'
@@ -1039,6 +1046,239 @@ if ENV['install_example'] == 'true'
1039
1046
  say "- Running migrations for owner + seed (owners + apartments.owner_id + 3 apts/3 owners)..."
1040
1047
  run "bundle exec rake db:migrate"
1041
1048
 
1049
+ # ---------------------------------------------------------------------
1050
+ # FormElementShowcase: a fourth example resource that exercises every
1051
+ # kept Tier 1 form_element helper on a single object. The kept set
1052
+ # (and the rationale for what was dropped) is documented in
1053
+ # stuff/form-element-showcase-plan.md. The runtime helpers handle
1054
+ # every element generically; this block wires up the model, value
1055
+ # rows, uploaders, join table, and seed data.
1056
+ # ---------------------------------------------------------------------
1057
+ say "- Generating FormElementShowcase (one resource per kept Tier 1 form_element)..."
1058
+ sleep 1
1059
+ run %q{bundle exec rails g inline_forms FormElementShowcase title:string body_plain_area:plain_text_area count:integer_field price:decimal_field amount:money_field meeting_date:date_select meeting_time:time_select birth_month:month_select start_month:month_year_picker is_active:check_box gender:radio_button rating_int:dropdown_with_integers priority:dropdown_with_values priority2:dropdown_with_values stars:dropdown_with_values_with_stars scale_int:scale_with_integers scale_val:scale_with_values attachment:file_field jingle:audio_field cover:image_field description:rich_text roles:has_and_belongs_to_many _enabled:yes _list_order:title _list_search:title _presentation:'#{title}'}
1060
+
1061
+ say "- Generating Attachment + Jingle uploaders (Cover reuses ImageUploader)..."
1062
+ run "bundle exec rails generate uploader Attachment"
1063
+ run "bundle exec rails generate uploader Jingle"
1064
+
1065
+ # Cover reuses ImageUploader (already generated for Photo). CarrierWave
1066
+ # is happy to mount one uploader class on multiple models; the global
1067
+ # `remove_previously_stored_files_after_update = false` keeps PaperTrail
1068
+ # revertable for both.
1069
+ gsub_file "app/models/form_element_showcase.rb",
1070
+ "mount_uploader :cover, CoverUploader",
1071
+ "mount_uploader :cover, ImageUploader"
1072
+
1073
+ # money-rails wiring for the `amount:money_field` showcase column.
1074
+ #
1075
+ # * The inline_forms generator emits a plain `t.integer :amount`
1076
+ # (FormElementRegistry maps `:money_field` to `:integer`), but
1077
+ # money-rails' `monetize` reader/writer convention is `_cents`-
1078
+ # suffixed: declare `monetize :amount_cents`, store cents in
1079
+ # `amount_cents`, and money-rails defines `obj.amount`/`obj.amount=`
1080
+ # returning/parsing Money objects. That keeps the inline_forms
1081
+ # attribute list entry `[:amount, :money_field]` semantically
1082
+ # correct (`obj.amount` => Money) without a custom helper.
1083
+ # * Rename the column in the generated create-table migration from
1084
+ # `:amount` to `:amount_cents` *before* `rake db:migrate` runs.
1085
+ # * Add a small initializer pinning the default currency so the
1086
+ # showcase renders predictably under any host locale.
1087
+ showcase_migration = Dir.glob("db/migrate/*_inline_forms_create_form_element_showcases.rb").first
1088
+ if showcase_migration
1089
+ gsub_file showcase_migration, /t\.integer :amount\b/, "t.integer :amount_cents, default: 0, null: false"
1090
+ end
1091
+
1092
+ create_file "config/initializers/money.rb", <<-MONEY_INIT.strip_heredoc
1093
+ # Generated by inline_forms (money-rails defaults for the
1094
+ # FormElementShowcase `amount:money_field` demo column).
1095
+ MoneyRails.configure do |config|
1096
+ config.default_currency = :usd
1097
+ end
1098
+ MONEY_INIT
1099
+
1100
+ # First field-level validation in the example app. `allow_blank: true`
1101
+ # keeps PaperTrail revert paths (and the seeded second row, which leaves
1102
+ # count nil) valid. The integration test covers the explicit error
1103
+ # path by POSTing `count: "abc"` to /form_element_showcases.
1104
+ inject_into_file "app/models/form_element_showcase.rb",
1105
+ "\n validates :count, numericality: { only_integer: true }, allow_blank: true\n mount_uploader :attachment, AttachmentUploader\n monetize :amount_cents\n",
1106
+ after: "class FormElementShowcase < ApplicationRecord\n"
1107
+
1108
+ # Value-bearing rows for every form_element that needs a values hash
1109
+ # (or hash + options_disabled). 8.1.5 row shape: [:attr, :form_element]
1110
+ # for bare rows, [:attr, :form_element, values, options_disabled] for
1111
+ # choice rows.
1112
+ showcase_value_rows = {
1113
+ "[ :is_active, :check_box ]" => "[ :is_active, :check_box, { 0 => 'no', 1 => 'yes' } ]",
1114
+ "[ :gender, :radio_button ]" => "[ :gender, :radio_button, { 1 => 'male', 2 => 'female' } ]",
1115
+ "[ :rating_int, :dropdown_with_integers ]" => "[ :rating_int, :dropdown_with_integers, { 1 => 'one', 2 => 'two', 3 => 'three' } ]",
1116
+ "[ :priority, :dropdown_with_values ]" => "[ :priority, :dropdown_with_values, { 1 => 'low', 2 => 'mid', 3 => 'high' } ]",
1117
+ "[ :priority2, :dropdown_with_values ]" => "[ :priority2, :dropdown_with_values, { 1 => 'low', 2 => 'mid', 3 => 'high' }, [ 2 ] ]",
1118
+ "[ :stars, :dropdown_with_values_with_stars ]" => "[ :stars, :dropdown_with_values_with_stars, { 1 => '1stars', 2 => '2stars', 3 => '3stars', 4 => '4stars', 5 => '5stars' } ]",
1119
+ "[ :scale_int, :scale_with_integers ]" => "[ :scale_int, :scale_with_integers, { 1 => 'one', 2 => 'two', 3 => 'three', 4 => 'four', 5 => 'five' } ]",
1120
+ "[ :scale_val, :scale_with_values ]" => "[ :scale_val, :scale_with_values, { 1 => 'red', 2 => 'green', 3 => 'blue' } ]",
1121
+ }
1122
+ showcase_value_rows.each do |from, to|
1123
+ gsub_file "app/models/form_element_showcase.rb", from, to
1124
+ end
1125
+
1126
+ # Section headers + the trailing info/info_list block. `roles` is a
1127
+ # has_and_belongs_to_many relation so the generator skips its
1128
+ # attribute_list row; we hand-insert it as `:info_list` (read-only
1129
+ # checklist of role _presentation strings).
1130
+ showcase_header_inserts = {
1131
+ "[ :title, :text_field ]," => "[ :header_basics, :header ], \n [ :title, :text_field ],",
1132
+ "[ :count, :integer_field ]," => "[ :header_numbers, :header ], \n [ :count, :integer_field ],",
1133
+ "[ :meeting_date, :date_select ]," => "[ :header_dates, :header ], \n [ :meeting_date, :date_select ],",
1134
+ "[ :is_active, :check_box, { 0 => 'no'" => "[ :header_choices, :header ], \n [ :is_active, :check_box, { 0 => 'no'",
1135
+ "[ :attachment, :file_field ]," => "[ :header_files, :header ], \n [ :attachment, :file_field ],",
1136
+ "[ :description, :rich_text ]," => "[ :header_rich, :header ], \n [ :description, :rich_text ],",
1137
+ }
1138
+ showcase_header_inserts.each do |from, to|
1139
+ gsub_file "app/models/form_element_showcase.rb", from, to
1140
+ end
1141
+
1142
+ # Insert :roles (info_list), :header_meta, and the timestamps after
1143
+ # the rich_text row. The generator does not emit a row for
1144
+ # `roles:has_and_belongs_to_many` (relation? is true), so we add it
1145
+ # manually here.
1146
+ gsub_file "app/models/form_element_showcase.rb",
1147
+ "[ :description, :rich_text ], \n",
1148
+ "[ :description, :rich_text ], \n [ :roles, :info_list ], \n [ :header_meta, :header ], \n [ :created_at, :info ], \n [ :updated_at, :info ], \n"
1149
+
1150
+ # Locale keys for the showcase attributes. Headers + the timestamps
1151
+ # need explicit labels so `human_attribute_name` does not fall back to
1152
+ # "Header basics" / "Created at". The leading two-space indent puts
1153
+ # the `activerecord:` key under the existing `en:` root.
1154
+ showcase_locale = <<~END_SHOWCASE_LOCALE
1155
+ activerecord:
1156
+ attributes:
1157
+ form_element_showcase:
1158
+ header_basics: Basics
1159
+ header_numbers: Numbers
1160
+ header_dates: Dates and times
1161
+ header_choices: Choices and scales
1162
+ header_files: Files
1163
+ header_rich: Rich text
1164
+ header_meta: Metadata
1165
+ title: Title
1166
+ body_plain_area: Plain text area
1167
+ count: Count
1168
+ price: Price
1169
+ amount: Amount
1170
+ meeting_date: Meeting date
1171
+ meeting_time: Meeting time
1172
+ birth_month: Birth month
1173
+ start_month: Start month
1174
+ is_active: Is active
1175
+ gender: Gender
1176
+ rating_int: Rating
1177
+ priority: Priority
1178
+ priority2: Priority (with disabled option)
1179
+ stars: Stars
1180
+ scale_int: Scale (integers)
1181
+ scale_val: Scale (values)
1182
+ attachment: Attachment
1183
+ jingle: Jingle
1184
+ cover: Cover
1185
+ description: Description
1186
+ roles: Roles
1187
+ created_at: Created at
1188
+ updated_at: Updated at
1189
+ END_SHOWCASE_LOCALE
1190
+ append_to_file "config/locales/inline_forms_local.en.yml", showcase_locale.lines.map { |l| " #{l}" }.join
1191
+
1192
+ # Star images for dropdown_with_values_with_stars. The runtime helper
1193
+ # calls `image_tag("\#{n}stars.png")`, so we ship 5 tiny PNGs in
1194
+ # lib/installer_templates/example_app_assets and copy them into
1195
+ # app/assets/images at install time.
1196
+ showcase_assets_root = File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_assets")
1197
+ (1..5).each do |n|
1198
+ src = File.join(showcase_assets_root, "#{n}stars.png")
1199
+ copy_file src, File.join("app/assets/images", "#{n}stars.png") if File.exist?(src)
1200
+ end
1201
+
1202
+ # Join table for has_and_belongs_to_many :roles. Mirrors the
1203
+ # roles_users join migration created for the user model above.
1204
+ say "- Creating form_element_showcases_roles join migration..."
1205
+ sleep 1
1206
+ habtm_ts = Time.now.utc.strftime("%Y%m%d%H%M%S")
1207
+ create_file "db/migrate/#{habtm_ts}_create_join_table_form_element_showcases_roles.rb", <<-HABTM_MIGRATION.strip_heredoc
1208
+ class CreateJoinTableFormElementShowcasesRoles < ActiveRecord::Migration[8.1]
1209
+ def self.up
1210
+ create_table :form_element_showcases_roles, id: false, force: true do |t|
1211
+ t.integer :form_element_showcase_id
1212
+ t.integer :role_id
1213
+ end
1214
+ end
1215
+
1216
+ def self.down
1217
+ drop_table :form_element_showcases_roles
1218
+ end
1219
+ end
1220
+ HABTM_MIGRATION
1221
+
1222
+ # Seed two showcase rows: one fully populated (so every show branch
1223
+ # has data), and one with no roles + no uploads (so info_list's empty
1224
+ # branch and the uploader empty branches render). Idempotent.
1225
+ say "- Creating FormElementShowcase seed migration..."
1226
+ sleep 1
1227
+ showcase_seed_ts = Time.now.utc.strftime("%Y%m%d%H%M%S")
1228
+ create_file "db/migrate/#{showcase_seed_ts}_seed_form_element_showcases.rb", <<-SHOWCASE_SEED.strip_heredoc
1229
+ class SeedFormElementShowcases < ActiveRecord::Migration[8.1]
1230
+ def up
1231
+ return unless defined?(FormElementShowcase)
1232
+
1233
+ full = FormElementShowcase.find_or_create_by!(title: "Full demo") do |s|
1234
+ s.body_plain_area = "A short plain-text paragraph."
1235
+ s.count = 7
1236
+ s.price = "12.34"
1237
+ s.amount = Money.from_amount(99.95, "USD") if s.respond_to?(:amount=)
1238
+ s.meeting_date = Date.new(2026, 6, 1)
1239
+ s.meeting_time = Time.utc(2000, 1, 1, 14, 30)
1240
+ s.birth_month = 7
1241
+ s.start_month = Date.new(2026, 9, 1)
1242
+ s.is_active = true
1243
+ s.gender = 1
1244
+ s.rating_int = 2
1245
+ s.priority = 2
1246
+ s.priority2 = 3
1247
+ s.stars = 4
1248
+ s.scale_int = 3
1249
+ s.scale_val = 2
1250
+ s.description = "<p>A rich-text paragraph for the showcase.</p>"
1251
+ end
1252
+ if defined?(Role) && Role.exists?(1) && full.roles.empty?
1253
+ full.roles << Role.find(1)
1254
+ end
1255
+
1256
+ # "Empty" refers to the role and uploader fields (their empty
1257
+ # branches need to render). The other fields keep valid values
1258
+ # because several show helpers (e.g. dropdown_with_integers) raise
1259
+ # on nil/out-of-range integers.
1260
+ FormElementShowcase.find_or_create_by!(title: "Empty demo") do |s|
1261
+ s.is_active = false
1262
+ s.gender = 1
1263
+ s.rating_int = 1
1264
+ s.priority = 1
1265
+ s.priority2 = 1
1266
+ s.stars = 1
1267
+ s.scale_int = 1
1268
+ s.scale_val = 1
1269
+ end
1270
+ end
1271
+
1272
+ def down
1273
+ return unless defined?(FormElementShowcase)
1274
+ FormElementShowcase.where(title: ["Full demo", "Empty demo"]).destroy_all
1275
+ end
1276
+ end
1277
+ SHOWCASE_SEED
1278
+
1279
+ say "- Running showcase migrations (create table + join + seed)..."
1280
+ run "bundle exec rake db:migrate"
1281
+
1042
1282
  example_views_root = File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_views")
1043
1283
  Dir.glob(File.join(example_views_root, "**", "*")).sort.each do |abs|
1044
1284
  next unless File.file?(abs)
@@ -1,6 +1,6 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module InlineFormsInstaller
3
- VERSION = "8.1.5"
3
+ VERSION = "8.1.8"
4
4
 
5
5
  # Written into generated apps' `.ruby-version` (must match gemspec `required_ruby_version`).
6
6
  TARGET_RUBY_VERSION = "ruby-4.0.4"
@@ -0,0 +1,152 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Choice/scale helpers on FormElementShowcase:
6
+ # check_box (is_active, 0|1)
7
+ # radio_button (gender, hash)
8
+ # dropdown_with_integers (rating_int)
9
+ # dropdown_with_values (priority normal + priority2 options_disabled)
10
+ # dropdown_with_values_with_stars (stars)
11
+ # scale_with_integers (scale_int)
12
+ # scale_with_values (scale_val)
13
+ class ExampleAppShowcaseChoiceScaleFieldsTest < ExampleAppIntegrationTestCase
14
+ setup do
15
+ @showcase = FormElementShowcase.find_or_create_by!(title: "Choice/scale demo")
16
+ end
17
+
18
+ def field_frame(attr)
19
+ "form_element_showcase_#{@showcase.id}_#{attr}"
20
+ end
21
+
22
+ def field_headers(attr)
23
+ { "Turbo-Frame" => field_frame(attr), "Accept" => "text/html" }
24
+ end
25
+
26
+ def underscored_param_root
27
+ "_form_element_showcase"
28
+ end
29
+
30
+ test "check_box toggles is_active from 0 to 1" do
31
+ @showcase.update!(is_active: false)
32
+ frame = field_frame(:is_active)
33
+ put form_element_showcase_path(
34
+ @showcase,
35
+ attribute: "is_active",
36
+ form_element: "check_box",
37
+ update: frame
38
+ ), params: { is_active: 1 }, headers: field_headers(:is_active)
39
+
40
+ assert_response :success
41
+ assert_equal 1, @showcase.reload.is_active ? 1 : 0
42
+ end
43
+
44
+ test "check_box toggles is_active back to 0 when param missing" do
45
+ @showcase.update!(is_active: true)
46
+ frame = field_frame(:is_active)
47
+ put form_element_showcase_path(
48
+ @showcase,
49
+ attribute: "is_active",
50
+ form_element: "check_box",
51
+ update: frame
52
+ ), params: {}, headers: field_headers(:is_active)
53
+
54
+ assert_response :success
55
+ assert_equal false, !!@showcase.reload.is_active
56
+ end
57
+
58
+ test "radio_button sets gender via attribute param" do
59
+ frame = field_frame(:gender)
60
+ put form_element_showcase_path(
61
+ @showcase,
62
+ attribute: "gender",
63
+ form_element: "radio_button",
64
+ update: frame
65
+ ), params: { gender: 2 }, headers: field_headers(:gender)
66
+
67
+ assert_response :success
68
+ assert_equal 2, @showcase.reload.gender
69
+ end
70
+
71
+ test "dropdown_with_integers sets rating_int" do
72
+ frame = field_frame(:rating_int)
73
+ put form_element_showcase_path(
74
+ @showcase,
75
+ attribute: "rating_int",
76
+ form_element: "dropdown_with_integers",
77
+ update: frame
78
+ ), params: { underscored_param_root => { rating_int: 2 } }, headers: field_headers(:rating_int)
79
+
80
+ assert_response :success
81
+ assert_equal 2, @showcase.reload.rating_int
82
+ end
83
+
84
+ test "dropdown_with_values priority round-trips" do
85
+ frame = field_frame(:priority)
86
+ put form_element_showcase_path(
87
+ @showcase,
88
+ attribute: "priority",
89
+ form_element: "dropdown_with_values",
90
+ update: frame
91
+ ), params: { underscored_param_root => { priority: 3 } }, headers: field_headers(:priority)
92
+
93
+ assert_response :success
94
+ assert_equal 3, @showcase.reload.priority
95
+ end
96
+
97
+ test "dropdown_with_values priority2 disables the third option in the edit form" do
98
+ @showcase.update!(priority2: 1)
99
+ frame = field_frame(:priority2)
100
+ get edit_form_element_showcase_path(
101
+ @showcase,
102
+ attribute: "priority2",
103
+ form_element: "dropdown_with_values",
104
+ update: frame
105
+ ), headers: field_headers(:priority2)
106
+
107
+ assert_response :success
108
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
109
+ # options_disabled is [2] for priority2; that option should render with `disabled`.
110
+ assert_match(%r{<option [^>]*disabled[^>]*value="2"|<option [^>]*value="2"[^>]*disabled}, @response.body)
111
+ end
112
+
113
+ test "dropdown_with_values_with_stars sets stars" do
114
+ frame = field_frame(:stars)
115
+ put form_element_showcase_path(
116
+ @showcase,
117
+ attribute: "stars",
118
+ form_element: "dropdown_with_values_with_stars",
119
+ update: frame
120
+ ), params: { underscored_param_root => { stars: 5 } }, headers: field_headers(:stars)
121
+
122
+ assert_response :success
123
+ assert_equal 5, @showcase.reload.stars
124
+ end
125
+
126
+ test "scale_with_integers scale_int round-trips" do
127
+ frame = field_frame(:scale_int)
128
+ put form_element_showcase_path(
129
+ @showcase,
130
+ attribute: "scale_int",
131
+ form_element: "scale_with_integers",
132
+ update: frame
133
+ ), params: { underscored_param_root => { scale_int: 4 } }, headers: field_headers(:scale_int)
134
+
135
+ assert_response :success
136
+ assert_equal 4, @showcase.reload.scale_int
137
+ end
138
+
139
+ test "scale_with_values scale_val round-trips" do
140
+ frame = field_frame(:scale_val)
141
+ put form_element_showcase_path(
142
+ @showcase,
143
+ attribute: "scale_val",
144
+ form_element: "scale_with_values",
145
+ update: frame
146
+ ), params: { underscored_param_root => { scale_val: 3 } }, headers: field_headers(:scale_val)
147
+
148
+ assert_response :success
149
+ assert_equal 3, @showcase.reload.scale_val
150
+ end
151
+
152
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Date/time-shaped Tier 1 helpers on FormElementShowcase:
6
+ # date_select (meeting_date)
7
+ # time_select (meeting_time)
8
+ # month_select (birth_month, 1..12 integer)
9
+ # month_year_picker (start_month, date)
10
+ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
11
+ setup do
12
+ @showcase = FormElementShowcase.find_or_create_by!(title: "Date/time demo")
13
+ end
14
+
15
+ test "date_select meeting_date renders datepicker on edit" do
16
+ frame = "form_element_showcase_#{@showcase.id}_meeting_date"
17
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
18
+
19
+ get edit_form_element_showcase_path(
20
+ @showcase,
21
+ attribute: "meeting_date",
22
+ form_element: "date_select",
23
+ update: frame
24
+ ), headers: headers
25
+
26
+ assert_response :success
27
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
28
+ assert_includes @response.body, %(class="datepicker")
29
+ assert_includes @response.body, %(name="meeting_date")
30
+ end
31
+
32
+ test "time_select meeting_time renders timepicker on edit" do
33
+ @showcase.update!(meeting_time: Time.utc(2000, 1, 1, 9, 15))
34
+ frame = "form_element_showcase_#{@showcase.id}_meeting_time"
35
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
36
+
37
+ get edit_form_element_showcase_path(
38
+ @showcase,
39
+ attribute: "meeting_time",
40
+ form_element: "time_select",
41
+ update: frame
42
+ ), headers: headers
43
+
44
+ assert_response :success
45
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
46
+ assert_includes @response.body, %(class="timepicker")
47
+ assert_includes @response.body, %(name="meeting_time")
48
+ end
49
+
50
+ test "month_select birth_month renders 12 options on edit" do
51
+ @showcase.update!(birth_month: 6)
52
+ frame = "form_element_showcase_#{@showcase.id}_birth_month"
53
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
54
+
55
+ get edit_form_element_showcase_path(
56
+ @showcase,
57
+ attribute: "birth_month",
58
+ form_element: "month_select",
59
+ update: frame
60
+ ), headers: headers
61
+
62
+ assert_response :success
63
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
64
+ assert_match(/<select[^>]+name="date\[birth_month\]"/, @response.body)
65
+ end
66
+
67
+ test "month_year_picker start_month renders datepicker class hook" do
68
+ @showcase.update!(start_month: Date.new(2026, 5, 1))
69
+ frame = "form_element_showcase_#{@showcase.id}_start_month"
70
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
71
+
72
+ get edit_form_element_showcase_path(
73
+ @showcase,
74
+ attribute: "start_month",
75
+ form_element: "month_year_picker",
76
+ update: frame
77
+ ), headers: headers
78
+
79
+ assert_response :success
80
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
81
+ assert_includes @response.body, %(datepicker-month-year)
82
+ assert_includes @response.body, %(name="start_month")
83
+ end
84
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Numeric Tier 1 helpers on FormElementShowcase:
6
+ # integer_field (count) + numericality validation regression
7
+ # decimal_field (price)
8
+ # money_field (amount, money-rails `monetize :amount_cents`)
9
+ class ExampleAppShowcaseNumericFieldsTest < ExampleAppIntegrationTestCase
10
+ setup do
11
+ @showcase = FormElementShowcase.find_or_create_by!(title: "Numeric demo")
12
+ @list_frame = "form_element_showcases_list"
13
+ @list_headers = { "Turbo-Frame" => @list_frame, "Accept" => "text/html" }
14
+ end
15
+
16
+ test "integer_field count round-trips" do
17
+ frame = "form_element_showcase_#{@showcase.id}_count"
18
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
19
+
20
+ put form_element_showcase_path(
21
+ @showcase,
22
+ attribute: "count",
23
+ form_element: "integer_field",
24
+ update: frame
25
+ ), params: { count: "42" }, headers: headers
26
+
27
+ assert_response :success
28
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
29
+ assert_includes @response.body, "42"
30
+ assert_equal 42, @showcase.reload.count
31
+ end
32
+
33
+ test "decimal_field price round-trips" do
34
+ frame = "form_element_showcase_#{@showcase.id}_price"
35
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
36
+
37
+ put form_element_showcase_path(
38
+ @showcase,
39
+ attribute: "price",
40
+ form_element: "decimal_field",
41
+ update: frame
42
+ ), params: { price: "99.95" }, headers: headers
43
+
44
+ assert_response :success
45
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
46
+ assert_includes @response.body, "99.95"
47
+ assert_equal "99.95", @showcase.reload.price
48
+ end
49
+
50
+ test "money_field amount round-trips through monetize :amount_cents" do
51
+ skip "money-rails monetize not configured" unless FormElementShowcase.respond_to?(:monetized_attributes) &&
52
+ FormElementShowcase.monetized_attributes.key?("amount")
53
+
54
+ frame = "form_element_showcase_#{@showcase.id}_amount"
55
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
56
+
57
+ put form_element_showcase_path(
58
+ @showcase,
59
+ attribute: "amount",
60
+ form_element: "money_field",
61
+ update: frame
62
+ ), params: { amount: "12.34" }, headers: headers
63
+
64
+ assert_response :success
65
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
66
+ reloaded = @showcase.reload
67
+ assert_equal 1234, reloaded.amount_cents,
68
+ "expected monetize to parse '12.34' into 1234 cents, got #{reloaded.amount_cents.inspect}"
69
+ end
70
+
71
+ test "integer_field count rejects non-integer via top-level create" do
72
+ # The controller's create iterates every attribute and calls each
73
+ # form_element's update method. Several helpers used to crash on nil
74
+ # params (month_year_picker_update, scale_*_update, dropdown_*_update)
75
+ # so we supply just enough to let the create walk reach validation.
76
+ assert_no_difference "FormElementShowcase.count" do
77
+ post form_element_showcases_path(update: @list_frame),
78
+ params: {
79
+ title: "Bad Count",
80
+ count: "abc",
81
+ amount: "0.00",
82
+ start_month: "September 2026",
83
+ _form_element_showcase: {
84
+ rating_int: 1,
85
+ priority: 1,
86
+ priority2: 1,
87
+ stars: 1,
88
+ scale_int: 1,
89
+ scale_val: 1,
90
+ },
91
+ },
92
+ headers: @list_headers
93
+ end
94
+ assert_response :success
95
+ body = @response.body
96
+ assert_match(/is not a number|count[^<]*is not a number/i, body,
97
+ "expected numericality error to render after invalid count")
98
+ end
99
+ end
@@ -0,0 +1,124 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Smoke test for the full FormElementShowcase show page. Covers the
6
+ # display-only helpers (`header`, `info`, `info_list`) and the show
7
+ # branches of every uploader helper + rich_text.
8
+ class ExampleAppShowcasePageRenderTest < ExampleAppIntegrationTestCase
9
+ # All non-header attributes on FormElementShowcase that should each
10
+ # be wrapped in a `<turbo-frame id="form_element_showcase_<id>_<attr>">`.
11
+ SHOWCASE_FIELD_ATTRIBUTES = %i[
12
+ title
13
+ body_plain_area
14
+ count
15
+ price
16
+ amount
17
+ meeting_date
18
+ meeting_time
19
+ birth_month
20
+ start_month
21
+ is_active
22
+ gender
23
+ rating_int
24
+ priority
25
+ priority2
26
+ stars
27
+ scale_int
28
+ scale_val
29
+ attachment
30
+ jingle
31
+ cover
32
+ description
33
+ roles
34
+ created_at
35
+ updated_at
36
+ ].freeze
37
+
38
+ SHOWCASE_HEADER_ATTRIBUTES = %i[
39
+ header_basics
40
+ header_numbers
41
+ header_dates
42
+ header_choices
43
+ header_files
44
+ header_rich
45
+ header_meta
46
+ ].freeze
47
+
48
+ setup do
49
+ locale = Locale.find_or_create_by!(name: "en") { |l| l.title = "English" }
50
+ @role = Role.find_or_create_by!(name: "superadmin") { |r| r.description = "Super Admin" }
51
+
52
+ @full = FormElementShowcase.find_or_create_by!(title: "Full demo") do |s|
53
+ s.body_plain_area = "Plain text body"
54
+ s.count = 7
55
+ s.price = "12.34"
56
+ s.meeting_date = Date.new(2026, 6, 1)
57
+ s.meeting_time = Time.utc(2000, 1, 1, 14, 30)
58
+ s.birth_month = 7
59
+ s.start_month = Date.new(2026, 9, 1)
60
+ s.is_active = true
61
+ s.gender = 1
62
+ s.rating_int = 2
63
+ s.priority = 2
64
+ s.priority2 = 3
65
+ s.stars = 4
66
+ s.scale_int = 3
67
+ s.scale_val = 2
68
+ s.amount = Money.from_amount(99.95, "USD") if s.respond_to?(:amount=) && defined?(Money)
69
+ s.description = "<p>A rich-text body.</p>"
70
+ end
71
+ @full.roles << @role unless @full.roles.where(id: @role.id).exists?
72
+
73
+ @empty = FormElementShowcase.find_or_create_by!(title: "Empty demo") do |s|
74
+ # Keep dropdown/scale integers at valid indices so the show helpers
75
+ # do not crash on nil. The point of "empty demo" is that roles and
76
+ # uploads are blank, not every integer attribute.
77
+ s.gender = 1
78
+ s.rating_int = 1
79
+ s.priority = 1
80
+ s.priority2 = 1
81
+ s.stars = 1
82
+ s.scale_int = 1
83
+ s.scale_val = 1
84
+ end
85
+ end
86
+
87
+ test "full showcase show page renders every per-attribute turbo-frame" do
88
+ row_frame = "form_element_showcase_#{@full.id}"
89
+ get form_element_showcase_path(@full, update: row_frame),
90
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
91
+
92
+ assert_response :success
93
+
94
+ SHOWCASE_FIELD_ATTRIBUTES.each do |attr|
95
+ frame = "form_element_showcase_#{@full.id}_#{attr}"
96
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">),
97
+ "expected per-attribute frame for #{attr}"
98
+ end
99
+
100
+ SHOWCASE_HEADER_ATTRIBUTES.each do |attr|
101
+ label = FormElementShowcase.human_attribute_name(attr)
102
+ assert_includes @response.body, label,
103
+ "expected header label for #{attr} (#{label.inspect})"
104
+ end
105
+
106
+ assert_includes @response.body, @role.name,
107
+ "expected info_list to render the role's _presentation"
108
+ end
109
+
110
+ test "empty showcase info_list renders the no-roles placeholder" do
111
+ row_frame = "form_element_showcase_#{@empty.id}"
112
+ get form_element_showcase_path(@empty, update: row_frame),
113
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
114
+
115
+ assert_response :success
116
+ frame = "form_element_showcase_#{@empty.id}_roles"
117
+ body = @response.body
118
+ assert_includes body, %(<turbo-frame id="#{frame}">)
119
+ frame_block = body[body.index(%(<turbo-frame id="#{frame}">"))..-1] rescue nil
120
+ # info_list_show emits a `--` placeholder for empty associations.
121
+ assert_match(/<turbo-frame id="#{frame}">[^<]*<div class='row [^']+'>--<\/div>/m, body,
122
+ "expected info_list empty placeholder `--` inside roles frame")
123
+ end
124
+ end
@@ -0,0 +1,47 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Round-trip the text-shaped Tier 1 helpers on FormElementShowcase:
6
+ # text_field (title)
7
+ # plain_text_area (body_plain_area)
8
+ class ExampleAppShowcaseTextFieldsTest < ExampleAppIntegrationTestCase
9
+ setup do
10
+ @showcase = FormElementShowcase.find_or_create_by!(title: "TextFields demo")
11
+ end
12
+
13
+ test "text_field title round-trips through the field turbo-frame" do
14
+ frame = "form_element_showcase_#{@showcase.id}_title"
15
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
16
+
17
+ put form_element_showcase_path(
18
+ @showcase,
19
+ attribute: "title",
20
+ form_element: "text_field",
21
+ update: frame
22
+ ), params: { title: "TextFields updated" }, headers: headers
23
+
24
+ assert_response :success
25
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
26
+ assert_includes @response.body, "TextFields updated"
27
+ assert_equal "TextFields updated", @showcase.reload.title
28
+ end
29
+
30
+ test "plain_text_area body round-trips through the field turbo-frame" do
31
+ frame = "form_element_showcase_#{@showcase.id}_body_plain_area"
32
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
33
+
34
+ body = "Hello plain text\nMulti-line content"
35
+ put form_element_showcase_path(
36
+ @showcase,
37
+ attribute: "body_plain_area",
38
+ form_element: "plain_text_area",
39
+ update: frame
40
+ ), params: { body_plain_area: body }, headers: headers
41
+
42
+ assert_response :success
43
+ assert_includes @response.body, %(<turbo-frame id="#{frame}">)
44
+ assert_includes @response.body, "Hello plain text"
45
+ assert_equal body, @showcase.reload.body_plain_area
46
+ end
47
+ end
@@ -0,0 +1,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "test_helper"
4
+
5
+ # Pins the FormElementShowcase model wiring:
6
+ # - plain_text columns are present (no rich_text drift)
7
+ # - every kept Tier 1 attribute is in the attribute list
8
+ # - priority2 keeps its options_disabled tuple slot (8.1.5 row shape)
9
+ # - integer numericality is enforced on :count
10
+ class ExampleAppFormElementShowcaseTest < ActiveSupport::TestCase
11
+ EXPECTED_ATTRIBUTES = %i[
12
+ header_basics
13
+ title
14
+ body_plain_area
15
+ header_numbers
16
+ count
17
+ price
18
+ amount
19
+ header_dates
20
+ meeting_date
21
+ meeting_time
22
+ birth_month
23
+ start_month
24
+ header_choices
25
+ is_active
26
+ gender
27
+ rating_int
28
+ priority
29
+ priority2
30
+ stars
31
+ scale_int
32
+ scale_val
33
+ header_files
34
+ attachment
35
+ jingle
36
+ cover
37
+ header_rich
38
+ description
39
+ roles
40
+ header_meta
41
+ created_at
42
+ updated_at
43
+ ].freeze
44
+
45
+ test "plain_text helper configuration validates without drift" do
46
+ assert_nothing_raised do
47
+ InlineForms.validate_plain_text_configuration_for!(FormElementShowcase)
48
+ end
49
+ end
50
+
51
+ test "inline_forms_attribute_list contains every expected attribute" do
52
+ keys = FormElementShowcase.new.inline_forms_attribute_list.map { |row| row.first }
53
+ EXPECTED_ATTRIBUTES.each do |attr|
54
+ assert_includes keys, attr, "expected #{attr} in inline_forms_attribute_list"
55
+ end
56
+ end
57
+
58
+ test "priority2 keeps options_disabled at tuple slot 3 (8.1.5 row shape)" do
59
+ row = FormElementShowcase.new.inline_forms_attribute_list.assoc(:priority2)
60
+ assert_not_nil row, "expected a :priority2 row"
61
+ assert_equal :dropdown_with_values, row[1]
62
+ assert_kind_of Hash, row[2]
63
+ assert_equal [2], row[3],
64
+ "expected options_disabled at index 3 (was the 8.1.5 row-shape shift)"
65
+ end
66
+
67
+ test "count rejects non-integer input with a numericality error" do
68
+ showcase = FormElementShowcase.new(title: "X", count: "abc")
69
+ assert_not showcase.valid?
70
+ assert_includes showcase.errors.attribute_names, :count
71
+ assert showcase.errors[:count].any? { |m| m.match?(/not a number/i) },
72
+ "expected `not a number` error on :count, got #{showcase.errors[:count].inspect}"
73
+ end
74
+
75
+ test "count accepts a valid integer" do
76
+ showcase = FormElementShowcase.new(title: "X", count: 5)
77
+ assert showcase.valid?, "expected showcase to be valid with count=5, got #{showcase.errors.full_messages.inspect}"
78
+ end
79
+
80
+ test "count allows blank" do
81
+ showcase = FormElementShowcase.new(title: "X", count: nil)
82
+ assert showcase.valid?, "expected showcase to be valid with count=nil (allow_blank), got #{showcase.errors.full_messages.inspect}"
83
+ end
84
+ end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: inline_forms_installer
3
3
  version: !ruby/object:Gem::Version
4
- version: 8.1.5
4
+ version: 8.1.8
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -89,6 +89,11 @@ files:
89
89
  - lib/installer_templates/dartsass/devise_main.scss
90
90
  - lib/installer_templates/dartsass/inline_forms_dartsass_builds.rb
91
91
  - lib/installer_templates/dartsass/inline_forms_main.scss
92
+ - lib/installer_templates/example_app_assets/1stars.png
93
+ - lib/installer_templates/example_app_assets/2stars.png
94
+ - lib/installer_templates/example_app_assets/3stars.png
95
+ - lib/installer_templates/example_app_assets/4stars.png
96
+ - lib/installer_templates/example_app_assets/5stars.png
92
97
  - lib/installer_templates/example_app_tests/test/example_app/example_integration_test_case.rb
93
98
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_field_turbo_test.rb
94
99
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_name_list_test.rb
@@ -104,10 +109,16 @@ files:
104
109
  - lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb
105
110
  - lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb
106
111
  - lib/installer_templates/example_app_tests/test/integration/example_app_routing_test.rb
112
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_choice_scale_fields_test.rb
113
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_date_time_fields_test.rb
114
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_numeric_fields_test.rb
115
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_page_render_test.rb
116
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_text_fields_test.rb
107
117
  - lib/installer_templates/example_app_tests/test/integration/example_app_turbo_layout_test.rb
108
118
  - lib/installer_templates/example_app_tests/test/integration/example_app_validation_hints_test.rb
109
119
  - lib/installer_templates/example_app_tests/test/models/example_app_apartment_name_validation_test.rb
110
120
  - lib/installer_templates/example_app_tests/test/models/example_app_apartment_photo_test.rb
121
+ - lib/installer_templates/example_app_tests/test/models/example_app_form_element_showcase_test.rb
111
122
  - lib/installer_templates/example_app_tests/test/models/example_app_paper_trail_changeset_test.rb
112
123
  - lib/installer_templates/example_app_tests/test/models/example_app_plain_text_rich_text_edge_cases_test.rb
113
124
  - lib/installer_templates/example_app_views/apartments/name_list.html.erb