inline_forms_installer 8.1.21 → 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 (28) hide show
  1. checksums.yaml +4 -4
  2. data/inline_forms_installer.gemspec +9 -9
  3. data/lib/inline_forms_installer/creator.rb +11 -7
  4. data/lib/inline_forms_installer/installer_core.rb +187 -44
  5. data/lib/inline_forms_installer/user_model_config.rb +2 -1
  6. data/lib/inline_forms_installer/version.rb +2 -1
  7. data/lib/inline_forms_installer.rb +1 -0
  8. data/lib/installer_templates/example_app_assets/seed_images/apt1_bedroom.png +0 -0
  9. data/lib/installer_templates/example_app_assets/seed_images/apt1_kitchen.png +0 -0
  10. data/lib/installer_templates/example_app_assets/seed_images/apt1_living.png +0 -0
  11. data/lib/installer_templates/example_app_assets/seed_images/apt2_kitchen.png +0 -0
  12. data/lib/installer_templates/example_app_assets/seed_images/apt2_living.png +0 -0
  13. data/lib/installer_templates/example_app_assets/seed_images/apt2_terrace.png +0 -0
  14. data/lib/installer_templates/example_app_assets/seed_images/apt3_living.png +0 -0
  15. data/lib/installer_templates/example_app_assets/seed_images/apt3_loft.png +0 -0
  16. data/lib/installer_templates/example_app_assets/seed_images/apt3_view.png +0 -0
  17. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_addto_field_test.rb +54 -0
  18. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_opening_date_test.rb +7 -6
  19. data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_top_level_new_test.rb +28 -0
  20. data/lib/installer_templates/example_app_tests/test/integration/example_app_photo_comments_test.rb +90 -0
  21. data/lib/installer_templates/example_app_tests/test/integration/example_app_production_ssl_test.rb +19 -0
  22. data/lib/installer_templates/example_app_tests/test/integration/example_app_schema_edit_test.rb +75 -0
  23. data/lib/installer_templates/example_app_tests/test/integration/example_app_showcase_date_time_fields_test.rb +69 -6
  24. data/lib/installer_templates/example_app_tests/test/integration/example_app_showcase_gallery_test.rb +68 -0
  25. data/lib/installer_templates/example_app_tests/test/integration/example_app_user_locale_test.rb +46 -0
  26. data/lib/installer_templates/example_app_tests/test/integration/example_app_user_theme_test.rb +76 -0
  27. data/lib/installer_templates/example_app_views/owners/_owner_tabs.html.erb +3 -3
  28. metadata +17 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 0da00593938ac34e84b4460a363616ed7dcb411e78886a391680f18f36f67ced
4
- data.tar.gz: 8608daaccd3e9406f722a2d4bfcb5b0971489e77edfa2f698b05a6b5489b5e17
3
+ metadata.gz: d09ceda3c59a9d785f9a7270e58254f1ace6e43ed605f9681e8046c1ad67f8a7
4
+ data.tar.gz: 35178d8b5bf044d7f941efa611bd39512f3060bc996b7669a12ed9e8c6721e6a
5
5
  SHA512:
6
- metadata.gz: 80ba56a1b679fe91baef7cfba8b405e7dd6311aaa7e57b2f835c6efb95a394e61cf082e96346888711c61c9ef35803ca58e074d7bb22383d9ad2e7c45a48cd7d
7
- data.tar.gz: 2d294b8be77f2f5a5cafe412c9d01cbcf33d72a76efeb531be97bf68c4a957f4b86a7f758feabbf72e172eb1bdadd99850a92de0c7e60bec408be4798ba68de3
6
+ metadata.gz: c57d71b4fa224a31135385f43175c50fcf4e54611965797726b3a8ea4dd89233e9bcac54a2f4180fbf8a5c692f9cc0b7710adee7040bca89dd291c59c43fe178
7
+ data.tar.gz: 7ca6d416410ef9d1342c6bd6339b3be6f6bdda2923465d1653d357729c0b3b94a90b2f00ed98e33f855fda4722d0b5a0580436c5358fdae278119d9bf9661ce1
@@ -1,4 +1,5 @@
1
1
  # -*- encoding: utf-8 -*-
2
+
2
3
  $:.push File.expand_path("../lib", __FILE__)
3
4
  require "inline_forms/gem_files"
4
5
  require "inline_forms_installer/version"
@@ -7,17 +8,17 @@ Gem::Specification.new do |s|
7
8
  s.name = "inline_forms_installer"
8
9
  s.version = InlineFormsInstaller::VERSION
9
10
  s.platform = Gem::Platform::RUBY
10
- s.authors = ["Ace Suares", "Lemuel Boyce", "Manuel Ortega"]
11
- s.email = ["ace@suares.com"]
12
- s.homepage = %q{http://github.com/acesuares/inline_forms}
13
- s.summary = %q{CLI and Rails app template for generating inline_forms applications.}
14
- s.description = %q{Installs the `inline_forms` CLI and scaffolds opinionated Rails apps with Devise, CanCan, PaperTrail, and optional example data.}
15
- s.licenses = ["MIT"]
11
+ s.authors = [ "Ace Suares", "Lemuel Boyce", "Manuel Ortega" ]
12
+ s.email = [ "ace@suares.com" ]
13
+ s.homepage = %q(http://github.com/acesuares/inline_forms)
14
+ s.summary = %q(CLI and Rails app template for generating inline_forms applications.)
15
+ s.description = %q(Installs the `inline_forms` CLI and scaffolds opinionated Rails apps with Devise, CanCan, PaperTrail, and optional example data.)
16
+ s.licenses = [ "MIT" ]
16
17
  s.required_ruby_version = ">= 4.0.0"
17
18
 
18
19
  s.files = InlineFormsGemFiles.gem_files(include_installer: true)
19
- s.executables = ["inline_forms"]
20
- s.require_paths = ["lib"]
20
+ s.executables = [ "inline_forms" ]
21
+ s.require_paths = [ "lib" ]
21
22
 
22
23
  s.add_dependency("inline_forms", "~> 8")
23
24
  s.add_dependency("thor", ">= 1.0", "< 2.0")
@@ -27,5 +28,4 @@ Gem::Specification.new do |s|
27
28
  # otherwise it installs without RVM (any version manager, or none, works —
28
29
  # Bundler isolates deps per app via Gemfile.lock). Install `gem install rvm`
29
30
  # to opt in to gemset integration, or pass `--no-rvm` to skip it explicitly.
30
-
31
31
  end
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require "securerandom"
3
4
  require "thor"
4
5
  require_relative "create_log"
@@ -16,13 +17,14 @@ module InlineFormsInstaller
16
17
  end
17
18
 
18
19
  desc "create APP", "create an application with inline_forms v#{InlineFormsInstaller.inline_forms_version}"
19
- DATABASE_OPTIONS = %w(sqlite mysql)
20
- method_option :database, :aliases => "-d", :banner => DATABASE_OPTIONS.join("|"), :desc => "specify development database"
21
- method_option :example, :type => :boolean, :desc => "install the example app. uses sqlite as development database"
22
- method_option :email, :aliases => "-e", :default => "admin@example.com", :desc => "specify admin email"
23
- method_option :password, :aliases => "-p", :default => "admin999", :desc => "specify admin password"
24
- method_option :skiprvm, :aliases => "--no-rvm", :type => :boolean, :default => false, :desc => "install inline_forms without RVM"
25
- method_option :user_model, :aliases => "-U", :default => "User", :banner => "CLASS", :desc => "Devise model class (e.g. Member); Warden scope stays :user (current_user)"
20
+ DATABASE_OPTIONS = %w[sqlite mysql]
21
+ method_option :database, aliases: "-d", banner: DATABASE_OPTIONS.join("|"), desc: "specify development database"
22
+ method_option :example, type: :boolean, desc: "install the example app. uses sqlite as development database"
23
+ method_option :schema_edit, type: :boolean, default: false, desc: "mount the schema-change GUI (inline_forms_schema_edit gem); implied by --example"
24
+ method_option :email, aliases: "-e", default: "admin@example.com", desc: "specify admin email"
25
+ method_option :password, aliases: "-p", default: "admin999", desc: "specify admin password"
26
+ method_option :skiprvm, aliases: "--no-rvm", type: :boolean, default: false, desc: "install inline_forms without RVM"
27
+ method_option :user_model, aliases: "-U", default: "User", banner: "CLASS", desc: "Devise model class (e.g. Member); Warden scope stays :user (current_user)"
26
28
 
27
29
  def create(app_name)
28
30
  def self.skiprvm
@@ -124,6 +126,8 @@ module InlineFormsInstaller
124
126
  ENV["using_sqlite"] = using_sqlite?.to_s
125
127
  ENV["database"] = database
126
128
  ENV["install_example"] = install_example?.to_s
129
+ # The example app always mounts the schema GUI (it demos + tests it).
130
+ ENV["install_schema_edit"] = (options[:schema_edit] || install_example?).to_s
127
131
  # Match the generated `.ruby-version` to the version manager actually in
128
132
  # use. RVM's `.ruby-version` reader needs the `ruby-X.Y.Z` form — a bare
129
133
  # `X.Y.Z` makes `rvm use .` bail with "do not know how to handle" and skip
@@ -45,7 +45,7 @@ def install_prerelease_gems_from_roots!
45
45
  roots.uniq!
46
46
  return if roots.empty?
47
47
 
48
- %w[validation_hints inline_forms inline_forms_installer].each do |name|
48
+ %w[validation_hints inline_forms inline_forms_installer inline_forms_schema_edit].each do |name|
49
49
  # Pick the *highest version*, not the highest filename. String sort
50
50
  # placed `inline_forms-8.1.7.gem` above `inline_forms-8.1.10.gem`
51
51
  # because "7" > "1" lexicographically — silently picking up a stale
@@ -62,7 +62,13 @@ def install_prerelease_gems_from_roots!
62
62
  # the exact shape that bit us between 8.1.6 (default gemset's
63
63
  # highest installer) and 8.1.10.
64
64
  candidates = roots.flat_map { |root|
65
- Dir[File.join(root, "#{name}-*.gem"), File.join(root, "pkg", "#{name}-*.gem")]
65
+ # inline_forms_schema_edit lives in a subdirectory of the inline_forms
66
+ # checkout with its own pkg/; glob both layouts for every name (the
67
+ # extra patterns simply match nothing for the root-level gems).
68
+ Dir[File.join(root, "#{name}-*.gem"),
69
+ File.join(root, "pkg", "#{name}-*.gem"),
70
+ File.join(root, name, "#{name}-*.gem"),
71
+ File.join(root, name, "pkg", "#{name}-*.gem")]
66
72
  }
67
73
  gem_file = candidates.max_by do |path|
68
74
  ver_str = File.basename(path, ".gem").sub(/\A#{Regexp.escape(name)}-/, "")
@@ -103,7 +109,7 @@ end
103
109
 
104
110
  add_source 'https://rubygems.org'
105
111
 
106
- gem 'cancancan'
112
+ gem 'cancancan', '~> 3.6'
107
113
  gem 'carrierwave', '~> 3.1'
108
114
  gem 'devise', '~> 5.0'
109
115
  gem 'devise-i18n', '~> 1.16'
@@ -119,12 +125,23 @@ if ENV["INLINE_FORMS_GEMFILE_PATH"] && File.directory?(ENV["INLINE_FORMS_GEMFILE
119
125
  else
120
126
  gem "inline_forms", "~> 8"
121
127
  end
128
+ # Schema-change GUI (separate mountable engine gem): only when requested via
129
+ # `--schema-edit` (implied by `--example`). Apps that never change their own
130
+ # schema ship without this surface entirely.
131
+ if ENV["install_schema_edit"] == "true"
132
+ schema_edit_path = ENV["INLINE_FORMS_GEMFILE_PATH"] &&
133
+ File.join(ENV["INLINE_FORMS_GEMFILE_PATH"], "inline_forms_schema_edit")
134
+ if schema_edit_path && File.directory?(schema_edit_path)
135
+ gem "inline_forms_schema_edit", path: schema_edit_path
136
+ else
137
+ gem "inline_forms_schema_edit", "~> 8"
138
+ end
139
+ end
140
+ # jQuery is required by Foundation 6's JS only. jQuery UI is fully gone since
141
+ # 8.1.26 (native date/time/month inputs in 8.1.25; datalist combobox + range
142
+ # slider in 8.1.26), as are jquery-timepicker-rails, jquery-ui-rails and
143
+ # rails-jquery-autocomplete.
122
144
  gem 'jquery-rails'
123
- gem 'jquery-timepicker-rails'
124
- # jQuery UI JavaScript (`//= require jquery.ui.all` in inline_forms.js). SCSS + PNGs
125
- # are vendored in the inline_forms engine (Dart Sass cannot evaluate sass-rails
126
- # `image-path()`). Pin matches former jquery-ui-sass-rails 4.0.3.x stack.
127
- gem 'jquery-ui-rails', '4.0.3'
128
145
  # Foundation Icons SCSS + fonts are vendored in the inline_forms engine (Dart Sass;
129
146
  # foundation-icons-sass-rails depended on sass-rails).
130
147
  gem 'mini_magick'
@@ -138,7 +155,6 @@ gem 'money-rails', '~> 3.0'
138
155
  gem 'mysql2'
139
156
  gem 'paper_trail', '~> 17.0'
140
157
  gem 'rails-i18n', '~> 8.1'
141
- gem 'rails-jquery-autocomplete'
142
158
  gem 'rails', '~> 8.1'
143
159
  gem 'rake'
144
160
  # NOTE: the `rvm` gem is intentionally NOT a runtime dependency of generated
@@ -156,10 +172,11 @@ gem 'importmap-rails'
156
172
  # use `<turbo-frame>` + HTML responses (see docs/ujs-to-turbo.md). Registers the
157
173
  # `turbo_stream` MIME type for optional stream responses.
158
174
  gem 'turbo-rails'
159
- gem 'tabs_on_rails', '~> 3.0'
175
+ # tabs (set_tab / tabs_tag) are vendored in the inline_forms engine since
176
+ # 8.1.23 (InlineForms::Tabs) — the unmaintained tabs_on_rails gem is gone.
160
177
  gem 'unicorn'
161
178
  gem 'validation_hints', '~> 8'
162
- gem 'will_paginate'
179
+ gem 'will_paginate', '~> 4.0'
163
180
 
164
181
  gem_group :test do
165
182
  # Rails 7 still expects Minitest 5; 6.x breaks the railties test runner.
@@ -353,6 +370,13 @@ class #{user_cfg.devise_migration_class} < ActiveRecord::Migration[8.1]
353
370
 
354
371
  t.string :name
355
372
  t.integer :locale_id
373
+ # Preset UI theme (inline_forms 8.1.29+): 0 default, 1 dark, 2 sepia,
374
+ # 3 high-contrast — see #{user_cfg.class_name}::INLINE_FORMS_THEMES.
375
+ t.integer :theme, default: 0, null: false
376
+ # Per-user color overrides (inline_forms 8.1.32+, theming Pattern 2):
377
+ # hex "#rrggbb" or NULL; rendered as --if-color-* overrides in <head>.
378
+ t.string :primary_color
379
+ t.string :accent_color
356
380
 
357
381
  t.timestamps
358
382
  end
@@ -396,6 +420,10 @@ create_file user_cfg.model_path, <<-USER_MODEL.strip_heredoc
396
420
 
397
421
  # validations
398
422
  validates :name, :presence => true
423
+ # Theming Pattern 2 color overrides; the color_field element only ever
424
+ # submits "#rrggbb" or nil, but validate anyway (console edits, imports).
425
+ validates :primary_color, format: { with: /\\A#\\h{6}\\z/i }, allow_blank: true
426
+ validates :accent_color, format: { with: /\\A#\\h{6}\\z/i }, allow_blank: true
399
427
 
400
428
  # Default ordering for inline_forms list views (and any explicit caller
401
429
  # via `#{user_cfg.class_name}.inline_forms_list`). Avoids `default_scope`
@@ -415,12 +443,32 @@ create_file user_cfg.model_path, <<-USER_MODEL.strip_heredoc
415
443
  return !!self.roles.find_by_name(role)
416
444
  end
417
445
 
446
+ # Preset UI themes (inline_forms Pattern 1 theming). The integer `theme`
447
+ # column maps to a body class `theme-<name>`; the palettes ship in the
448
+ # inline_forms engine (_theme.scss). The engine layout calls
449
+ # #inline_forms_theme on current_user when the method exists.
450
+ INLINE_FORMS_THEMES = { 0 => 'default', 1 => 'dark', 2 => 'sepia', 3 => 'high-contrast' }.freeze
451
+
452
+ def inline_forms_theme
453
+ INLINE_FORMS_THEMES.fetch(theme.to_i, 'default')
454
+ end
455
+
456
+ # Theming Pattern 2: --if-color-* overrides for this user, rendered as an
457
+ # inline <style> by the engine layout (inline_forms_user_color_overrides_style).
458
+ # Keys are --if-color- suffixes; values must be "#rrggbb".
459
+ def inline_forms_color_overrides
460
+ { 'primary' => primary_color, 'accent' => accent_color }.compact
461
+ end
462
+
418
463
  def inline_forms_attribute_list
419
464
  @inline_forms_attribute_list ||= [
420
465
  [ :header_user_login, :header ],
421
466
  [ :name, :text_field ],
422
467
  [ :email, :text_field ],
423
468
  [ :locale, :dropdown ],
469
+ [ :theme, :dropdown_with_values, { 0 => 'default', 1 => 'dark', 2 => 'sepia', 3 => 'high contrast' } ],
470
+ [ :primary_color, :color_field ],
471
+ [ :accent_color, :color_field ],
424
472
  [ :password, :devise_password_field ],
425
473
  [ :header_user_roles, :header ],
426
474
  [ :roles, :check_list ],
@@ -654,24 +702,36 @@ create_file "app/controllers/application_controller.rb", <<-END_APPCONTROLLER.st
654
702
  end
655
703
  # Comment previous lines if you don't want Devise authentication
656
704
 
657
- # Uncomment next line if you want I18n (based on subdomain)
658
- # before_action :set_locale
659
-
660
- # Uncomment next line and specify default locale
661
- # I18n.default_locale = :en
705
+ # Per-user locale (inline_forms 8.1.31): the signed-in user's Locale
706
+ # record (its `name`: en/nl/...) drives I18n for the request. Unknown or
707
+ # blank names fall back to I18n.default_locale, and I18n.with_locale
708
+ # restores the previous locale afterwards, so nothing leaks across
709
+ # requests or threads. Users pick their locale on their own user panel
710
+ # (the [:locale, :dropdown] row).
711
+ around_action :switch_locale
712
+
713
+ def switch_locale(&action)
714
+ candidate = current_user&.locale&.name.to_s.strip
715
+ locale =
716
+ if I18n.available_locales.map(&:to_s).include?(candidate)
717
+ candidate
718
+ else
719
+ I18n.default_locale
720
+ end
721
+ I18n.with_locale(locale, &action)
722
+ end
662
723
 
663
- # Uncomment next line and specify available locales
664
- # I18n.available_locales = [ :en, :nl, :pp ]
724
+ # Restrict/extend the locale whitelist in config/application.rb:
725
+ # config.i18n.available_locales = [:en, :nl]
726
+ # config.i18n.default_locale = :en
665
727
 
666
- # Uncomment next nine line if you want locale based on subdomain, like 'it.example.com, de.example.com'
667
- # def set_locale
668
- # I18n.locale = extract_locale_from_subdomain || I18n.default_locale
669
- # end
670
- #
671
- # def extract_locale_from_subdomain
728
+ # Uncomment if you prefer locale based on subdomain ('nl.example.com')
729
+ # instead of the per-user setting above:
730
+ # around_action :switch_locale_from_subdomain
731
+ # def switch_locale_from_subdomain(&action)
672
732
  # locale = request.subdomains.first
673
- # return nil if locale.nil?
674
- # I18n.available_locales.include?(locale.to_sym) ? locale.to_s : nil
733
+ # locale = nil unless locale && I18n.available_locales.map(&:to_s).include?(locale)
734
+ # I18n.with_locale(locale || I18n.default_locale, &action)
675
735
  # end
676
736
  end
677
737
  END_APPCONTROLLER
@@ -698,6 +758,19 @@ create_file "app/models/ability.rb", <<-END_ABILITY.strip_heredoc
698
758
  end
699
759
  END_ABILITY
700
760
 
761
+ # SSL in production. `rails new` only enables these when Kamal is skipped
762
+ # (its proxy config takes over otherwise), but generated apps deploy via the
763
+ # shipped Capistrano/Unicorn setup behind an SSL-terminating reverse proxy —
764
+ # and the Devise mailer config below already assumes https. Enabling both
765
+ # matches the Rails 8 defaults and closes Brakeman's ForceSSL warning
766
+ # (full-gate CI scans the generated app). Serving plain HTTP internally?
767
+ # Re-comment these in your app's config/environments/production.rb.
768
+ say "- Enabling assume_ssl/force_ssl in environments/production.rb..."
769
+ gsub_file "config/environments/production.rb",
770
+ "# config.assume_ssl = true", "config.assume_ssl = true"
771
+ gsub_file "config/environments/production.rb",
772
+ "# config.force_ssl = true", "config.force_ssl = true"
773
+
701
774
  # devise mailer stuff
702
775
  say "- Injecting devise mailer stuff in environments/production.rb..."
703
776
  # strip_heredoc_with_indent(2) became strip_heredoc(2), but only in rails 4... :-(
@@ -776,6 +849,32 @@ copy_file File.join(INSTALLER_ROOT,'lib/installer_templates/capistrano/Capfile')
776
849
  say "- Unicorn Config..."
777
850
  copy_file File.join(INSTALLER_ROOT,'lib/installer_templates/unicorn/production.rb'), "config/unicorn/production.rb"
778
851
 
852
+ # Schema-change GUI (dev-only authoring; InlineForms::SchemaController from
853
+ # the inline_forms_schema_edit gem). Wired only when requested via
854
+ # `--schema-edit` (implied by `--example`): routes + a "+ field" nav link.
855
+ # Applies inline_forms_addto through the browser; does NOT run db:migrate.
856
+ # Must precede the example section: its test gate exercises these routes.
857
+ if ENV['install_schema_edit'] == 'true'
858
+ say "- Schema GUI: routes + nav link + batch tables..."
859
+ # One line: the gem owns its route set, so gem upgrades can add routes
860
+ # without editing the app's routes.rb.
861
+ route 'InlineFormsSchemaEdit.draw_routes(self)'
862
+
863
+ # Batch-pipeline tables (drafted intents + batches). The generator writes
864
+ # the migration; migrate here for sqlite (mysql apps migrate by hand, same
865
+ # policy as the main install).
866
+ run "bundle exec rails g inline_forms_schema_edit:install"
867
+ run "bundle exec rake db:migrate" if ENV['using_sqlite'] == 'true'
868
+
869
+ if File.exist?("app/views/_inline_forms_tabs.html.erb")
870
+ inject_into_file "app/views/_inline_forms_tabs.html.erb",
871
+ " <li class=\"menu-text\">\n" \
872
+ " <a href=\"/schema/new\" data-turbo=\"false\" title=\"Add a field to a model (dev only)\">+ field</a>\n" \
873
+ " </li>\n",
874
+ before: " </ul>\n </div>\n <div class=\"top-bar-right\">"
875
+ end
876
+ end
877
+
779
878
  # Git
780
879
  say "- adding and committing to git..."
781
880
 
@@ -785,8 +884,16 @@ git commit: " -a -m 'Initial Commit'"
785
884
  # example
786
885
  if ENV['install_example'] == 'true'
787
886
  say "\nInstalling example application..."
788
- run 'bundle exec rails g inline_forms Photo name:string caption:string image:image_field description:rich_text apartment:belongs_to _list_order:name _presentation:\'#{name}\''
887
+ run 'bundle exec rails g inline_forms Photo name:string caption:string image:image_field description:rich_text apartment:belongs_to comments:has_many comments:associated _list_order:name _presentation:\'#{name}\''
789
888
  run 'bundle exec rails generate uploader Image'
889
+
890
+ # Comment: second nesting level (Apartment -> Photo -> Comment) so the
891
+ # example app demonstrates :associated panels inside an opened nested row.
892
+ # Like Photo it has no `_enabled`, so it is not accessible through
893
+ # top-level HTML — only through the comments panel on an opened photo.
894
+ say "- Generating Comment model (nested under Photo -- nesting-in-nesting demo)..."
895
+ sleep 1
896
+ run 'bundle exec rails g inline_forms Comment body:text_area photo:belongs_to _list_order:body _presentation:\'#{body}\''
790
897
  run 'bundle exec rails g inline_forms Apartment name:string title:string opening_date:date description:rich_text photos:has_many photos:associated _enabled:yes _list_order:name _list_search:name _presentation:\'#{name}\''
791
898
 
792
899
  say "- Apartment name is required..."
@@ -861,23 +968,18 @@ if ENV['install_example'] == 'true'
861
968
 
862
969
  run 'bundle exec rake db:migrate'
863
970
 
864
- # Seed the photos gallery from a local `pics/` folder. The folder is
865
- # *gitignored* in the gem source (so the built .gem stays small and
866
- # the gallery images are not committed) which means INSTALLER_ROOT/pics
867
- # exists only when the installer is run from the source repo, not when
868
- # it is run from an installed gem on the developer's box. We therefore
869
- # check, in order:
870
- # 1. ENV['INLINE_FORMS_SEED_PICS'] -- explicit override path
871
- # 2. INSTALLER_ROOT/pics -- monorepo / installer checkout
872
- # 3. /home/code/inline_forms/pics -- local dev convention
873
- # and copy whichever is found into the generated app's db/seed_images/.
874
- # The migration generated below is what reads from db/seed_images at
875
- # `db:migrate` / `db:test:prepare` time, so this copy is only ever a
876
- # one-shot at app generation.
971
+ # Seed the photos gallery. The 9 CC0 placeholder images (solid pastel +
972
+ # "Apt N / Room" label, ~4 KB each, generated with ImageMagick — see
973
+ # stuff/2026-07-06-forgejo-ci-rubocop-brakeman.md) ship with the installer
974
+ # under lib/installer_templates/example_app_assets/seed_images/, so every
975
+ # install — laptop, CI container, anywhere — gets the same gallery. Before
976
+ # 8.1.35 this looked for a *gitignored* local pics/ folder, which existed
977
+ # only on the maintainer's box: gem installs elsewhere (e.g. the Forgejo
978
+ # full-gate runner) silently produced zero photos and 12 test failures.
979
+ # ENV['INLINE_FORMS_SEED_PICS'] still overrides with your own folder.
877
980
  pics_candidates = [
878
981
  ENV["INLINE_FORMS_SEED_PICS"],
879
- File.join(INSTALLER_ROOT, "pics"),
880
- "/home/code/inline_forms/pics",
982
+ File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_assets/seed_images"),
881
983
  ].compact
882
984
  pics_src = pics_candidates.find { |p| Dir.exist?(p) }
883
985
  if pics_src
@@ -1043,6 +1145,13 @@ if ENV['install_example'] == 'true'
1043
1145
  apartments: [] },
1044
1146
  ].freeze
1045
1147
 
1148
+ # Comments seeded on each apartment's first photo (Apartment ->
1149
+ # Photo -> Comment: the second nesting level).
1150
+ COMMENTS = [
1151
+ "Lovely light in this room!",
1152
+ "Could use a fresh coat of paint.",
1153
+ ].freeze
1154
+
1046
1155
  def up
1047
1156
  seed_dir = Rails.root.join("db", "seed_images")
1048
1157
  all_pics = seed_dir.directory? ?
@@ -1073,6 +1182,15 @@ if ENV['install_example'] == 'true'
1073
1182
  end
1074
1183
  end
1075
1184
 
1185
+ # Nesting-in-nesting demo data: two comments on each apartment's
1186
+ # first photo, so the comments panel inside an opened photo row
1187
+ # is visibly populated out of the box.
1188
+ if (first_photo = apt.photos.order(:id).first)
1189
+ COMMENTS.each do |body|
1190
+ Comment.find_or_create_by!(photo: first_photo, body: body)
1191
+ end
1192
+ end
1193
+
1076
1194
  apt_records[spec[:name]] = apt
1077
1195
  end
1078
1196
 
@@ -1099,6 +1217,7 @@ if ENV['install_example'] == 'true'
1099
1217
  APARTMENTS.each do |spec|
1100
1218
  apt = Apartment.find_by(name: spec[:name])
1101
1219
  next unless apt
1220
+ apt.photos.each { |photo| photo.comments.destroy_all }
1102
1221
  apt.photos.destroy_all
1103
1222
  apt.destroy
1104
1223
  end
@@ -1119,7 +1238,7 @@ if ENV['install_example'] == 'true'
1119
1238
  # ---------------------------------------------------------------------
1120
1239
  say "- Generating FormElementShowcase (one resource per kept Tier 1 form_element)..."
1121
1240
  sleep 1
1122
- 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 latitude:decimal_field{9,6} longitude:decimal_field{10,6} 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 locales:has_and_belongs_to_many _enabled:yes _list_order:title _list_search:title _presentation:'#{title}'}
1241
+ 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 latitude:decimal_field{9,6} longitude:decimal_field{10,6} 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 gallery:multi_image_field description:rich_text locales:has_and_belongs_to_many _enabled:yes _list_order:title _list_search:title _presentation:'#{title}'}
1123
1242
 
1124
1243
  say "- Generating Attachment + Jingle uploaders (Cover reuses ImageUploader)..."
1125
1244
  run "bundle exec rails generate uploader Attachment"
@@ -1150,6 +1269,9 @@ if ENV['install_example'] == 'true'
1150
1269
  showcase_migration = Dir.glob("db/migrate/*_inline_forms_create_form_element_showcases.rb").first
1151
1270
  if showcase_migration
1152
1271
  gsub_file showcase_migration, /t\.integer :amount\b/, "t.integer :amount_cents, default: 0, null: false"
1272
+ # multi_image_field stores a JSON array of filenames (mount_uploaders);
1273
+ # the registry maps it to :string, widen to :text for the serialized list.
1274
+ gsub_file showcase_migration, /t\.string :gallery\b/, "t.text :gallery"
1153
1275
  end
1154
1276
 
1155
1277
  create_file "config/initializers/money.rb", <<-MONEY_INIT.strip_heredoc
@@ -1185,7 +1307,9 @@ if ENV['install_example'] == 'true'
1185
1307
  " validates :price, numericality: true, allow_blank: true\n" \
1186
1308
  " validates :latitude, numericality: { greater_than_or_equal_to: -90, less_than_or_equal_to: 90 }, allow_blank: true\n" \
1187
1309
  " validates :longitude, numericality: { greater_than_or_equal_to: -180, less_than_or_equal_to: 180 }, allow_blank: true\n" \
1188
- " mount_uploader :attachment, AttachmentUploader\n monetize :amount_cents\n\n def locales_display\n locales\n end\n",
1310
+ " mount_uploader :attachment, AttachmentUploader\n" \
1311
+ " serialize :gallery, coder: JSON\n mount_uploaders :gallery, ImageUploader\n" \
1312
+ " monetize :amount_cents\n\n def locales_display\n locales\n end\n",
1189
1313
  after: "class FormElementShowcase < ApplicationRecord\n"
1190
1314
 
1191
1315
  # Value-bearing rows for every form_element that needs a values hash
@@ -1272,6 +1396,7 @@ if ENV['install_example'] == 'true'
1272
1396
  attachment: Attachment
1273
1397
  jingle: Jingle
1274
1398
  cover: Cover
1399
+ gallery: Gallery (multiple images)
1275
1400
  description: Description
1276
1401
  locales: Locales (editable)
1277
1402
  locales_display: Locales (read-only)
@@ -1382,6 +1507,13 @@ if ENV['install_example'] == 'true'
1382
1507
  next if full.public_send(attr).present?
1383
1508
  File.open(path, "rb") { |io| full.public_send("\#{attr}=", io) }
1384
1509
  end
1510
+
1511
+ # multi_image_field demo: an *array* assignment through the plural
1512
+ # mount_uploaders. One file is enough to exercise the array path.
1513
+ gallery_src = seed_uploads.join("sample_cover.png")
1514
+ if full.respond_to?(:gallery) && gallery_src.file? && full.gallery.blank?
1515
+ File.open(gallery_src, "rb") { |io| full.gallery = [io] }
1516
+ end
1385
1517
  full.save! if full.changed?
1386
1518
 
1387
1519
  # "Empty" refers to the role and uploader fields (their empty
@@ -1410,6 +1542,16 @@ if ENV['install_example'] == 'true'
1410
1542
  say "- Running showcase migrations (create table + join + seed)..."
1411
1543
  run "bundle exec rake db:migrate"
1412
1544
 
1545
+ # End-to-end exercise of `rails g inline_forms_addto` on an existing model.
1546
+ # Apartment is _enabled and uses the stock inline_forms show (Owner's tabbed
1547
+ # controller whitelists fields, so a new field would be hidden there). This
1548
+ # proves generate + migrate + boot + render + update against a real app; the
1549
+ # accompanying integration test is example_app_apartment_addto_field_test.rb.
1550
+ say "- addto demo: adding Apartment#internal_note via inline_forms_addto..."
1551
+ sleep 1
1552
+ run "bundle exec rails g inline_forms_addto Apartment internal_note:string"
1553
+ run "bundle exec rake db:migrate"
1554
+
1413
1555
  example_views_root = File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_views")
1414
1556
  Dir.glob(File.join(example_views_root, "**", "*")).sort.each do |abs|
1415
1557
  next unless File.file?(abs)
@@ -1418,6 +1560,7 @@ if ENV['install_example'] == 'true'
1418
1560
  end
1419
1561
 
1420
1562
  route 'get "apartments/name_list", to: "apartments#name_list", as: :apartment_name_list'
1563
+
1421
1564
  route "root :to => 'apartments#index'"
1422
1565
 
1423
1566
  example_tests_root = File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_tests")
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require "active_support/core_ext/string/inflections"
3
4
 
4
5
  module InlineFormsInstaller
@@ -47,7 +48,7 @@ module InlineFormsInstaller
47
48
  def join_table
48
49
  return "roles_users" if default?
49
50
 
50
- [table_name, "roles"].sort.join("_")
51
+ [ table_name, "roles" ].sort.join("_")
51
52
  end
52
53
 
53
54
  def controller_name
@@ -1,6 +1,7 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  module InlineFormsInstaller
3
- VERSION = "8.1.21"
4
+ VERSION = "8.1.42"
4
5
 
5
6
  # Canonical bare Ruby version (must match gemspec `required_ruby_version`).
6
7
  # Written verbatim into generated apps' `.ruby-version` for rbenv/chruby/asdf/
@@ -1,4 +1,5 @@
1
1
  # -*- encoding : utf-8 -*-
2
+
2
3
  require "inline_forms_installer/version"
3
4
 
4
5
  module InlineFormsInstaller
@@ -0,0 +1,54 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # End-to-end coverage for `rails g inline_forms_addto Apartment
6
+ # internal_note:string`, which the installer runs during --example. Proves the
7
+ # generated column migrated, the new attribute_list row renders on the stock
8
+ # show panel, and an inline edit/update round-trips through the generic
9
+ # controller.
10
+ class ExampleAppApartmentAddtoFieldTest < ExampleAppIntegrationTestCase
11
+ setup do
12
+ @apartment = Apartment.find_or_create_by!(name: "Addto Field Apt") do |a|
13
+ a.title = "Addto Field Title"
14
+ end
15
+ @frame_id = "apartment_#{@apartment.id}_internal_note"
16
+ @turbo_headers = { "Turbo-Frame" => @frame_id }
17
+ end
18
+
19
+ test "internal_note column exists after the addto migration" do
20
+ assert_includes Apartment.column_names, "internal_note",
21
+ "inline_forms_addto migration should have added the internal_note column"
22
+ end
23
+
24
+ test "internal_note row renders on the stock show panel" do
25
+ row_frame = "apartment_#{@apartment.id}"
26
+ get apartment_path(@apartment, update: row_frame),
27
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
28
+
29
+ assert_response :success
30
+ assert_includes @response.body, @frame_id,
31
+ "the addto attribute_list row should render its own per-field turbo-frame"
32
+ end
33
+
34
+ test "internal_note inline edit and update round-trips" do
35
+ get edit_apartment_path(
36
+ @apartment,
37
+ attribute: "internal_note",
38
+ form_element: "text_field",
39
+ update: @frame_id
40
+ ), headers: @turbo_headers
41
+ assert_response :success
42
+ assert_includes @response.body, %(<turbo-frame id="#{@frame_id}">)
43
+
44
+ put apartment_path(
45
+ @apartment,
46
+ attribute: "internal_note",
47
+ form_element: "text_field",
48
+ update: @frame_id
49
+ ), params: { internal_note: "Boiler serviced 2026-07" }, headers: @turbo_headers
50
+ assert_response :success
51
+ assert_includes @response.body, "Boiler serviced 2026-07"
52
+ assert_equal "Boiler serviced 2026-07", @apartment.reload.internal_note
53
+ end
54
+ end
@@ -2,7 +2,7 @@
2
2
 
3
3
  require_relative "../example_app/example_integration_test_case"
4
4
 
5
- # Example app includes opening_date:date on Apartment (jQuery UI datepicker).
5
+ # Example app includes opening_date:date on Apartment (native date input since 8.1.25).
6
6
  class ExampleAppApartmentOpeningDateTest < ExampleAppIntegrationTestCase
7
7
  setup do
8
8
  @apartment = Apartment.find_or_create_by!(name: "Datepicker Apt") do |a|
@@ -24,7 +24,7 @@ class ExampleAppApartmentOpeningDateTest < ExampleAppIntegrationTestCase
24
24
  assert_includes @response.body, "15-03-2019"
25
25
  end
26
26
 
27
- test "inline edit opening_date uses datepicker class hook" do
27
+ test "inline edit opening_date renders a native date input" do
28
28
  get edit_apartment_path(
29
29
  @apartment,
30
30
  attribute: "opening_date",
@@ -33,17 +33,18 @@ class ExampleAppApartmentOpeningDateTest < ExampleAppIntegrationTestCase
33
33
  ), headers: @turbo_headers
34
34
 
35
35
  assert_response :success
36
- assert_includes @response.body, %(class="datepicker")
36
+ assert_includes @response.body, %(type="date")
37
37
  assert_includes @response.body, %(name="opening_date")
38
+ assert_includes @response.body, %(value="2019-03-15"), "value must be ISO 8601"
38
39
  refute_includes @response.body, "<script",
39
- "datepicker init is centralized in inline_forms.js"
40
+ "native inputs need no per-field init script"
40
41
  end
41
42
 
42
- test "new apartment form includes datepicker for opening_date" do
43
+ test "new apartment form includes a native date input for opening_date" do
43
44
  get new_apartment_path(update: @list_frame), headers: @list_headers
44
45
 
45
46
  assert_response :success
46
47
  assert_includes @response.body, %(name="opening_date")
47
- assert_includes @response.body, %(class="datepicker")
48
+ assert_includes @response.body, %(type="date")
48
49
  end
49
50
  end
@@ -67,4 +67,32 @@ class ExampleAppApartmentTopLevelNewTest < ExampleAppIntegrationTestCase
67
67
  assert_includes @response.body, %(<turbo-frame id="#{@frame}">)
68
68
  assert_includes @response.body, name
69
69
  end
70
+
71
+ # "Open after create": Apartment has photos:associated, so a turbo-stream
72
+ # create response refreshes the list AND opens the new row, landing the user
73
+ # where the photos panel (only available on a persisted record) is usable.
74
+ test "top-level create with turbo-stream accept opens the new row" do
75
+ name = "AAA-OpenAfterCreate-#{SecureRandom.hex(4)}"
76
+ assert_difference("Apartment.count", 1) do
77
+ post apartments_path(update: @frame),
78
+ params: { name: name, title: "Open after create test" },
79
+ headers: { "Turbo-Frame" => @frame,
80
+ "Accept" => "text/vnd.turbo-stream.html, text/html" }
81
+ end
82
+ assert_response :success
83
+ assert_equal "text/vnd.turbo-stream.html", @response.media_type
84
+
85
+ apartment = Apartment.find_by!(name: name)
86
+ assert_includes @response.body,
87
+ %(<turbo-stream action="replace" target="#{@frame}">)
88
+ assert_includes @response.body,
89
+ %(<turbo-stream action="replace" target="apartment_#{apartment.id}">)
90
+ assert_includes @response.body,
91
+ %(id="apartment_#{apartment.id}_photos_list_auto_header")
92
+ assert_match(
93
+ %r{/photos/new\?[^"]*parent_id=#{apartment.id}},
94
+ @response.body,
95
+ "expected the photos panel's new link for the created apartment"
96
+ )
97
+ end
70
98
  end
@@ -0,0 +1,90 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Nesting-in-nesting (Apartment -> Photo -> Comment): a Photo opened inside an
6
+ # apartment's photos panel exposes its own :associated comments panel, and the
7
+ # whole nested CRUD contract works one level deeper.
8
+ class ExampleAppPhotoCommentsTest < ExampleAppIntegrationTestCase
9
+ setup do
10
+ @apartment = Apartment.create!(name: "Nested", title: "Nesting demo")
11
+ @photo = @apartment.photos.create!(name: "room.jpg", caption: "the room")
12
+ end
13
+
14
+ test "comments are not served as standalone html resource" do
15
+ assert Comment.not_accessible_through_html?
16
+ get comments_path
17
+ assert_not response.successful?,
18
+ "expected no standalone HTML index for not_accessible_through_html model (got #{response.status})"
19
+ end
20
+
21
+ test "opened nested photo row exposes the comments panel with its new link" do
22
+ row_frame = "apartment_#{@apartment.id}_photo_#{@photo.id}"
23
+ get photo_path(@photo, update: row_frame),
24
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
25
+ assert_response :success
26
+
27
+ panel = "photo_#{@photo.id}_comments"
28
+ assert_includes @response.body, %(id="#{panel}_list_auto_header")
29
+ assert_includes @response.body, %(<turbo-frame id="#{panel}")
30
+ assert_match(
31
+ %r{/comments/new\?[^"]*parent_id=#{@photo.id}},
32
+ @response.body,
33
+ "expected the comments panel's new-record link for the photo"
34
+ )
35
+ end
36
+
37
+ test "comments panel lists existing comments" do
38
+ comment = @photo.comments.create!(body: "First!")
39
+ row_frame = "apartment_#{@apartment.id}_photo_#{@photo.id}"
40
+ get photo_path(@photo, update: row_frame),
41
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
42
+ assert_response :success
43
+ assert_includes @response.body,
44
+ %(<turbo-frame id="photo_#{@photo.id}_comment_#{comment.id}">)
45
+ assert_includes @response.body, "First!"
46
+ end
47
+
48
+ test "new comment form renders inside the comments panel frame" do
49
+ panel = "photo_#{@photo.id}_comments"
50
+ get new_comment_path(update: panel, parent_class: "Photo", parent_id: @photo.id),
51
+ headers: { "Turbo-Frame" => panel, "Accept" => "text/html" }
52
+ assert_response :success
53
+ assert_includes @response.body, %(<turbo-frame id="#{panel}">)
54
+ assert_includes @response.body, 'name="body"'
55
+ end
56
+
57
+ test "nested comment create persists with the photo FK and returns the panel frame" do
58
+ panel = "photo_#{@photo.id}_comments"
59
+ assert_difference("Comment.count", 1) do
60
+ post comments_path(update: panel, parent_class: "Photo", parent_id: @photo.id),
61
+ params: { body: "Great view from here" },
62
+ headers: { "Turbo-Frame" => panel,
63
+ "Accept" => "text/vnd.turbo-stream.html, text/html" }
64
+ end
65
+ assert_response :success
66
+ # Comment has no :associated panel of its own, so open-after-create does
67
+ # not fire; the nested flow keeps the plain frame response.
68
+ assert_equal "text/html", @response.media_type
69
+ assert_includes @response.body, %(<turbo-frame id="#{panel}">)
70
+
71
+ comment = Comment.find_by!(body: "Great view from here")
72
+ assert_equal @photo.id, comment.photo_id
73
+ end
74
+
75
+ test "nested comment row opens and closes" do
76
+ comment = @photo.comments.create!(body: "Open me")
77
+ row_frame = "photo_#{@photo.id}_comment_#{comment.id}"
78
+
79
+ get comment_path(comment, update: row_frame),
80
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
81
+ assert_response :success
82
+ assert_includes @response.body, %(<turbo-frame id="#{row_frame}">)
83
+ assert_includes @response.body, "Open me"
84
+
85
+ get comment_path(comment, update: row_frame, close: true),
86
+ headers: { "Turbo-Frame" => row_frame, "Accept" => "text/html" }
87
+ assert_response :success
88
+ assert_includes @response.body, %(<turbo-frame id="#{row_frame}">)
89
+ end
90
+ end
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # The installer enables assume_ssl/force_ssl in the generated production
6
+ # config (8.1.36): generated apps deploy behind an SSL-terminating proxy and
7
+ # the Devise mailer config assumes https. Guards Brakeman's ForceSSL check
8
+ # (the full-gate CI scans this app) against a silent regression, e.g. the
9
+ # Rails template shifting its comment format so the installer's gsub misses.
10
+ class ExampleAppProductionSslTest < ExampleAppIntegrationTestCase
11
+ test "production config forces SSL" do
12
+ config = File.read(Rails.root.join("config", "environments", "production.rb"))
13
+
14
+ assert_match(/^\s*config\.assume_ssl = true/, config,
15
+ "expected the installer to enable config.assume_ssl in production.rb")
16
+ assert_match(/^\s*config\.force_ssl = true/, config,
17
+ "expected the installer to enable config.force_ssl in production.rb")
18
+ end
19
+ end
@@ -0,0 +1,75 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # The dev-only schema-change GUI (InlineForms::SchemaController), mounted only
6
+ # in the example app. Non-destructive coverage: the new form renders and a
7
+ # preview of a brand-new scalar field on Apartment renders with no schema
8
+ # change. Apply (the actual generator run) is covered by the engine dummy suite
9
+ # (recording executor) and inline_forms_addto_generator_test, so this test does
10
+ # not mutate the generated app during the shared gate.
11
+ class ExampleAppSchemaEditTest < ExampleAppIntegrationTestCase
12
+ test "schema/new renders the add-a-field form" do
13
+ get "/schema/new"
14
+ assert_response :success
15
+ assert_includes @response.body, "Form element"
16
+ assert_includes @response.body, "text_field"
17
+ end
18
+
19
+ test "schema/preview shows a new scalar field on Apartment without changing the schema" do
20
+ refute_includes Apartment.column_names, "staff_note",
21
+ "precondition: staff_note must not already exist"
22
+
23
+ post "/schema/preview",
24
+ params: { model_name: "Apartment", attribute: "staff_note",
25
+ form_element: "text_field", after: "name" }
26
+
27
+ assert_response :success
28
+ assert_includes @response.body, "staff_note (new)"
29
+ assert_includes @response.body, %(name="staff_note")
30
+
31
+ Apartment.reset_column_information
32
+ refute_includes Apartment.column_names, "staff_note",
33
+ "preview must not add a column"
34
+ end
35
+
36
+ # Batch pipeline (phases 1-3): the install generator created the tables,
37
+ # so draft -> submit -> token export works end to end in a generated app.
38
+ # No codegen happens (batch mode never generates in the request cycle), so
39
+ # this cannot mutate the app during the shared gate.
40
+ test "batch drafting, freeze on submit, and token-authenticated export" do
41
+ post "/schema/draft",
42
+ params: { model_name: "Apartment", attribute: "staff_note",
43
+ form_element: "text_field", after: "name", label: "Staff note" }
44
+ assert_response :redirect
45
+
46
+ batch = InlineForms::SchemaBatch.with_status(:draft).last
47
+ assert_equal 1, batch.intents.count
48
+
49
+ get "/schema"
50
+ assert_response :success
51
+ assert_includes @response.body, "staff_note"
52
+
53
+ post "/schema/batch/submit"
54
+ batch.reload
55
+ assert_equal "submitted", batch.status
56
+ assert_match(/\Asha256:/, batch.content_digest)
57
+ refute batch.intents.first.update(label: "changed"), "frozen after submit"
58
+
59
+ original_token = InlineFormsSchemaEdit.export_token
60
+ InlineFormsSchemaEdit.export_token = "gate-test-token"
61
+ begin
62
+ get "/schema/batches/#{batch.id}/export.json"
63
+ assert_response :unauthorized, "token configured but not provided"
64
+
65
+ get "/schema/batches/#{batch.id}/export.json",
66
+ headers: { "Authorization" => "Bearer gate-test-token" }
67
+ assert_response :success
68
+ payload = JSON.parse(@response.body)
69
+ assert_equal batch.content_digest, payload["digest"]
70
+ assert_equal "staff_note", payload["intents"].first["attribute"]
71
+ ensure
72
+ InlineFormsSchemaEdit.export_token = original_token
73
+ end
74
+ end
75
+ end
@@ -12,7 +12,8 @@ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
12
12
  @showcase = FormElementShowcase.find_or_create_by!(title: "Date/time demo")
13
13
  end
14
14
 
15
- test "date_select meeting_date renders datepicker on edit" do
15
+ test "date_select meeting_date renders native date input on edit" do
16
+ @showcase.update!(meeting_date: Date.new(2026, 5, 17))
16
17
  frame = "form_element_showcase_#{@showcase.id}_meeting_date"
17
18
  headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
18
19
 
@@ -25,11 +26,13 @@ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
25
26
 
26
27
  assert_response :success
27
28
  assert_includes @response.body, %(<turbo-frame id="#{frame}">)
28
- assert_includes @response.body, %(class="datepicker")
29
+ assert_includes @response.body, %(type="date")
29
30
  assert_includes @response.body, %(name="meeting_date")
31
+ assert_includes @response.body, %(value="2026-05-17"), "value must be ISO 8601"
32
+ refute_includes @response.body, "<script", "no inline picker init"
30
33
  end
31
34
 
32
- test "time_select meeting_time renders timepicker on edit" do
35
+ test "time_select meeting_time renders native time input on edit" do
33
36
  @showcase.update!(meeting_time: Time.utc(2000, 1, 1, 9, 15))
34
37
  frame = "form_element_showcase_#{@showcase.id}_meeting_time"
35
38
  headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
@@ -43,8 +46,42 @@ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
43
46
 
44
47
  assert_response :success
45
48
  assert_includes @response.body, %(<turbo-frame id="#{frame}">)
46
- assert_includes @response.body, %(class="timepicker")
49
+ assert_includes @response.body, %(type="time")
47
50
  assert_includes @response.body, %(name="meeting_time")
51
+ assert_includes @response.body, %(value="09:15"), "value must be 24h HH:MM"
52
+ end
53
+
54
+ test "time_select round-trips a native HH:MM submission" do
55
+ @showcase.update!(meeting_time: Time.utc(2000, 1, 1, 9, 15))
56
+ frame = "form_element_showcase_#{@showcase.id}_meeting_time"
57
+
58
+ put form_element_showcase_path(
59
+ @showcase,
60
+ attribute: "meeting_time",
61
+ form_element: "time_select",
62
+ update: frame
63
+ ), params: { meeting_time: "14:30" },
64
+ headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
65
+
66
+ assert_response :success
67
+ @showcase.reload
68
+ assert_equal 14, @showcase.meeting_time.hour
69
+ assert_equal 30, @showcase.meeting_time.min
70
+ end
71
+
72
+ test "date_select round-trips a native ISO submission" do
73
+ frame = "form_element_showcase_#{@showcase.id}_meeting_date"
74
+
75
+ put form_element_showcase_path(
76
+ @showcase,
77
+ attribute: "meeting_date",
78
+ form_element: "date_select",
79
+ update: frame
80
+ ), params: { meeting_date: "2027-01-09" },
81
+ headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
82
+
83
+ assert_response :success
84
+ assert_equal Date.new(2027, 1, 9), @showcase.reload.meeting_date
48
85
  end
49
86
 
50
87
  test "month_select birth_month renders 12 options on edit" do
@@ -64,7 +101,7 @@ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
64
101
  assert_match(/<select[^>]+name="date\[birth_month\]"/, @response.body)
65
102
  end
66
103
 
67
- test "month_year_picker start_month renders datepicker class hook" do
104
+ test "month_year_picker start_month renders native month input on edit" do
68
105
  @showcase.update!(start_month: Date.new(2026, 5, 1))
69
106
  frame = "form_element_showcase_#{@showcase.id}_start_month"
70
107
  headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
@@ -78,7 +115,33 @@ class ExampleAppShowcaseDateTimeFieldsTest < ExampleAppIntegrationTestCase
78
115
 
79
116
  assert_response :success
80
117
  assert_includes @response.body, %(<turbo-frame id="#{frame}">)
81
- assert_includes @response.body, %(datepicker-month-year)
118
+ assert_includes @response.body, %(type="month")
82
119
  assert_includes @response.body, %(name="start_month")
120
+ assert_includes @response.body, %(value="2026-05"), "value must be ISO YYYY-MM"
121
+ end
122
+
123
+ test "month_year_picker round-trips native YYYY-MM and legacy 'Month YYYY' submissions" do
124
+ frame = "form_element_showcase_#{@showcase.id}_start_month"
125
+ headers = { "Turbo-Frame" => frame, "Accept" => "text/html" }
126
+
127
+ # Native <input type="month"> value.
128
+ put form_element_showcase_path(
129
+ @showcase,
130
+ attribute: "start_month",
131
+ form_element: "month_year_picker",
132
+ update: frame
133
+ ), params: { start_month: "2026-09" }, headers: headers
134
+ assert_response :success
135
+ assert_equal Date.new(2026, 9, 1), @showcase.reload.start_month
136
+
137
+ # Legacy pre-8.1.25 display format still parses via the fallback.
138
+ put form_element_showcase_path(
139
+ @showcase,
140
+ attribute: "start_month",
141
+ form_element: "month_year_picker",
142
+ update: frame
143
+ ), params: { start_month: "November 2027" }, headers: headers
144
+ assert_response :success
145
+ assert_equal Date.new(2027, 11, 1), @showcase.reload.start_month
83
146
  end
84
147
  end
@@ -0,0 +1,68 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # multi_image_field (FormElementShowcase#gallery, 8.1.28): a mount_uploaders
6
+ # (plural) CarrierWave column serialized as a JSON array. First example-app
7
+ # coverage for this Tier 1 element.
8
+ class ExampleAppShowcaseGalleryTest < ExampleAppIntegrationTestCase
9
+ setup do
10
+ @showcase = FormElementShowcase.find_or_create_by!(title: "Gallery demo")
11
+ src = Rails.root.join("db", "seed_uploads", "sample_cover.png")
12
+ if @showcase.gallery.blank? && src.file?
13
+ File.open(src, "rb") { |io| @showcase.gallery = [io] }
14
+ @showcase.save!
15
+ end
16
+ end
17
+
18
+ test "show renders the gallery images from the plural uploader" do
19
+ skip "seed image missing" if @showcase.gallery.blank?
20
+
21
+ frame = "form_element_showcase_#{@showcase.id}"
22
+ get form_element_showcase_path(@showcase, update: frame),
23
+ headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
24
+
25
+ assert_response :success
26
+ assert_includes @response.body, %(<turbo-frame id="#{frame}_gallery">)
27
+ # mount_uploaders store_dir => uploads/form_element_showcase/gallery/:id/
28
+ assert_includes @response.body, "uploads/form_element_showcase/gallery",
29
+ "expected an <img> from the gallery uploader array"
30
+ end
31
+
32
+ test "edit renders a multiple file input submitting gallery[]" do
33
+ frame = "form_element_showcase_#{@showcase.id}_gallery"
34
+ get edit_form_element_showcase_path(
35
+ @showcase,
36
+ attribute: "gallery",
37
+ form_element: "multi_image_field",
38
+ update: frame
39
+ ), headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
40
+
41
+ assert_response :success
42
+ assert_includes @response.body, %(type="file")
43
+ assert_includes @response.body, %(name="gallery[]")
44
+ assert_includes @response.body, %(multiple="multiple")
45
+ end
46
+
47
+ test "empty gallery renders the inline-edit plus link" do
48
+ empty = FormElementShowcase.find_or_create_by!(title: "Gallery-less demo") do |s|
49
+ s.is_active = false
50
+ s.gender = 1
51
+ s.rating_int = 1
52
+ s.priority = 1
53
+ s.priority2 = 1
54
+ s.stars = 1
55
+ s.scale_int = 1
56
+ s.scale_val = 1
57
+ end
58
+ frame = "form_element_showcase_#{empty.id}"
59
+
60
+ get form_element_showcase_path(empty, update: frame),
61
+ headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
62
+
63
+ assert_response :success
64
+ gallery_frame = @response.body[/<turbo-frame id="#{frame}_gallery">.*?<\/turbo-frame>/m]
65
+ assert gallery_frame, "expected the gallery attribute frame"
66
+ assert_includes gallery_frame, "fi-plus"
67
+ end
68
+ end
@@ -0,0 +1,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Per-user locale (inline_forms 8.1.31): ApplicationController#switch_locale
6
+ # wraps every request in I18n.with_locale(current_user.locale.name), falling
7
+ # back to the default for unknown/blank names. Observable via the layout's
8
+ # <html lang="..."> attribute.
9
+ class ExampleAppUserLocaleTest < ExampleAppIntegrationTestCase
10
+ test "html lang reflects the default locale" do
11
+ get apartments_path
12
+
13
+ assert_response :success
14
+ assert_includes @response.body, %(<html lang="en">)
15
+ end
16
+
17
+ test "user locale preference switches I18n for the request" do
18
+ admin = User.find_by!(email: "admin@example.com")
19
+ nl = Locale.find_or_create_by!(name: "nl") { |l| l.title = "Nederlands" }
20
+ admin.update!(locale: nl)
21
+ sign_in admin
22
+
23
+ get apartments_path
24
+
25
+ assert_response :success
26
+ assert_includes @response.body, %(<html lang="nl">)
27
+ ensure
28
+ en = Locale.find_by(name: "en")
29
+ admin&.update!(locale: en) if en
30
+ end
31
+
32
+ test "unknown locale name falls back to the default" do
33
+ admin = User.find_by!(email: "admin@example.com")
34
+ xx = Locale.find_or_create_by!(name: "xx") { |l| l.title = "Not a locale" }
35
+ admin.update!(locale: xx)
36
+ sign_in admin
37
+
38
+ get apartments_path
39
+
40
+ assert_response :success
41
+ assert_includes @response.body, %(<html lang="en">)
42
+ ensure
43
+ en = Locale.find_by(name: "en")
44
+ admin&.update!(locale: en) if en
45
+ end
46
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # Pattern 1 preset theming (inline_forms 8.1.29): the user's integer `theme`
6
+ # column maps to a `theme-<name>` body class; palettes live in the engine's
7
+ # _theme.scss as --if-color-* custom-property overrides.
8
+ class ExampleAppUserThemeTest < ExampleAppIntegrationTestCase
9
+ test "body carries the default theme class" do
10
+ get apartments_path
11
+
12
+ assert_response :success
13
+ assert_includes @response.body, %(<body class="theme-default">)
14
+ end
15
+
16
+ test "user theme preference switches the body class" do
17
+ admin = User.find_by!(email: "admin@example.com")
18
+ admin.update!(theme: 1)
19
+ # Warden's test login caches the user *object* from setup's sign_in;
20
+ # re-sign-in so current_user carries the updated theme.
21
+ sign_in admin
22
+
23
+ get apartments_path
24
+
25
+ assert_response :success
26
+ assert_includes @response.body, %(<body class="theme-dark">)
27
+ ensure
28
+ admin&.update!(theme: 0)
29
+ end
30
+
31
+ test "high-contrast maps the css-safe class name" do
32
+ admin = User.find_by!(email: "admin@example.com")
33
+ admin.update!(theme: 3)
34
+ sign_in admin
35
+
36
+ get apartments_path
37
+
38
+ assert_response :success
39
+ assert_includes @response.body, %(<body class="theme-high-contrast">)
40
+ ensure
41
+ admin&.update!(theme: 0)
42
+ end
43
+
44
+ test "theme dropdown renders on the user show panel" do
45
+ admin = User.find_by!(email: "admin@example.com")
46
+ frame = "user_#{admin.id}"
47
+
48
+ get user_path(admin, update: frame),
49
+ headers: { "Turbo-Frame" => frame, "Accept" => "text/html" }
50
+
51
+ assert_response :success
52
+ assert_includes @response.body, %(<turbo-frame id="user_#{admin.id}_theme">)
53
+ assert_includes @response.body, %(<turbo-frame id="user_#{admin.id}_primary_color">)
54
+ end
55
+
56
+ test "user color overrides render as a scoped inline style (Pattern 2)" do
57
+ admin = User.find_by!(email: "admin@example.com")
58
+ admin.update!(primary_color: "#2563eb")
59
+ sign_in admin
60
+
61
+ get apartments_path
62
+
63
+ assert_response :success
64
+ assert_includes @response.body, %(id="inline_forms_user_color_overrides")
65
+ assert_includes @response.body, "body.theme-default { --if-color-primary: #2563eb; }"
66
+ ensure
67
+ admin&.update!(primary_color: nil)
68
+ end
69
+
70
+ test "no override style tag renders when the user has no custom colors" do
71
+ get apartments_path
72
+
73
+ assert_response :success
74
+ refute_includes @response.body, %(id="inline_forms_user_color_overrides")
75
+ end
76
+ end
@@ -2,9 +2,9 @@
2
2
  Owner sub-tabs (Turbo). Each tab link targets the surrounding row
3
3
  turbo-frame (id == @update_span) so clicking a tab re-fetches
4
4
  OwnersController#show?tab=... and swaps just that frame. Active-tab
5
- highlighting is driven by set_tab / current_tab? (tabs_on_rails).
6
- InlineForms::TurboTabsBuilder threads link_options: through to the
7
- anchor; upstream tabs_on_rails 3.0 can only annotate the surrounding li.
5
+ highlighting is driven by set_tab / current_tab? (InlineForms::Tabs,
6
+ vendored since 8.1.23). InlineForms::TurboTabsBuilder threads
7
+ link_options: through to the anchor.
8
8
  -%>
9
9
  <div class="row owner_tabs_row">
10
10
  <div class='medium-1 large-1 column'>&nbsp;</div>
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.21
4
+ version: 8.1.42
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -77,7 +77,17 @@ files:
77
77
  - lib/installer_templates/example_app_assets/sample.txt
78
78
  - lib/installer_templates/example_app_assets/sample.wav
79
79
  - lib/installer_templates/example_app_assets/sample_cover.png
80
+ - lib/installer_templates/example_app_assets/seed_images/apt1_bedroom.png
81
+ - lib/installer_templates/example_app_assets/seed_images/apt1_kitchen.png
82
+ - lib/installer_templates/example_app_assets/seed_images/apt1_living.png
83
+ - lib/installer_templates/example_app_assets/seed_images/apt2_kitchen.png
84
+ - lib/installer_templates/example_app_assets/seed_images/apt2_living.png
85
+ - lib/installer_templates/example_app_assets/seed_images/apt2_terrace.png
86
+ - lib/installer_templates/example_app_assets/seed_images/apt3_living.png
87
+ - lib/installer_templates/example_app_assets/seed_images/apt3_loft.png
88
+ - lib/installer_templates/example_app_assets/seed_images/apt3_view.png
80
89
  - lib/installer_templates/example_app_tests/test/example_app/example_integration_test_case.rb
90
+ - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_addto_field_test.rb
81
91
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_field_turbo_test.rb
82
92
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_name_list_test.rb
83
93
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_name_required_test.rb
@@ -89,11 +99,15 @@ files:
89
99
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_versions_turbo_test.rb
90
100
  - lib/installer_templates/example_app_tests/test/integration/example_app_guest_access_test.rb
91
101
  - lib/installer_templates/example_app_tests/test/integration/example_app_owner_tabs_test.rb
102
+ - lib/installer_templates/example_app_tests/test/integration/example_app_photo_comments_test.rb
92
103
  - lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb
93
104
  - lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb
105
+ - lib/installer_templates/example_app_tests/test/integration/example_app_production_ssl_test.rb
94
106
  - lib/installer_templates/example_app_tests/test/integration/example_app_routing_test.rb
107
+ - lib/installer_templates/example_app_tests/test/integration/example_app_schema_edit_test.rb
95
108
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_choice_scale_fields_test.rb
96
109
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_date_time_fields_test.rb
110
+ - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_gallery_test.rb
97
111
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_locales_associations_test.rb
98
112
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_money_update_test.rb
99
113
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_numeric_fields_test.rb
@@ -102,6 +116,8 @@ files:
102
116
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_text_fields_test.rb
103
117
  - lib/installer_templates/example_app_tests/test/integration/example_app_showcase_validation_hints_test.rb
104
118
  - lib/installer_templates/example_app_tests/test/integration/example_app_turbo_layout_test.rb
119
+ - lib/installer_templates/example_app_tests/test/integration/example_app_user_locale_test.rb
120
+ - lib/installer_templates/example_app_tests/test/integration/example_app_user_theme_test.rb
105
121
  - lib/installer_templates/example_app_tests/test/integration/example_app_validation_hints_test.rb
106
122
  - lib/installer_templates/example_app_tests/test/models/example_app_apartment_name_validation_test.rb
107
123
  - lib/installer_templates/example_app_tests/test/models/example_app_apartment_photo_test.rb