inline_forms_installer 7.11.0 → 7.13.11

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: 4f8e3a2820c839b5fb67dece7b04f1e24197a5e52fdef3d4644d92bb1335dcc3
4
- data.tar.gz: 4f1fb0bd62fa999f8d5c94092ce24f02aa9f39e854e5f84bfe151c3b0ad8b5db
3
+ metadata.gz: 6c28f4c0dd8adc114abc964fa0d7ae579c14f07ef01b00e7ae9c49817db3b3d6
4
+ data.tar.gz: a361b6d41eaf9a5c1a4534a52d596b8925a67f8dcf9020ff7254727818aa628b
5
5
  SHA512:
6
- metadata.gz: ef80b8f92079b27070b3246ccfea9d9740ba39aded15c8cc5a814a8d792e99c36cca4f7f3bf4884101f4c27dff366ea4b4ae8c20ad1846cc545da7340b0d466d
7
- data.tar.gz: ca5ce491b22353866f0dd7e92eee93dfdc7baf78d3f9a8e609b6264f0dcd1ac7c5d7f74212900f4d14c5f35562edc8b18cf44d5e2c30f9a8e913e46194d27fe4
6
+ metadata.gz: ab0fab2477b0ff7748dcf23374a6ead91d574eba165b64e654e0163b4c5c0b1e340b7c218e819f164ec87c0be399277b838134ad37c3344f9d372291a5a99fbb
7
+ data.tar.gz: 5d4996784f073975b11ed3a0bf5d3cd3473c9804e6ac934198e6a9109a3c596b862c1aaf1bfb365b18a00f094da78a02aed091422de6b544652886f5caa64ae1
@@ -13,12 +13,13 @@ Gem::Specification.new do |s|
13
13
  s.summary = %q{CLI and Rails app template for generating inline_forms applications.}
14
14
  s.description = %q{Installs the `inline_forms` CLI and scaffolds opinionated Rails apps with Devise, CanCan, PaperTrail, and optional example data.}
15
15
  s.licenses = ["MIT"]
16
- s.required_ruby_version = ">= 3.2.0"
16
+ s.required_ruby_version = ">= 4.0.0"
17
17
 
18
18
  s.files = InlineFormsGemFiles.gem_files(include_installer: true)
19
19
  s.executables = ["inline_forms"]
20
20
  s.require_paths = ["lib"]
21
21
 
22
+ s.add_dependency("inline_forms", "~> 7")
22
23
  s.add_dependency("rvm", ">= 1.11", "< 2.0")
23
24
  s.add_dependency("thor", ">= 1.0", "< 2.0")
24
25
  end
@@ -6,6 +6,10 @@ module InlineFormsInstaller
6
6
  class Creator < Thor
7
7
  include Thor::Actions
8
8
 
9
+ def self.exit_on_failure?
10
+ true
11
+ end
12
+
9
13
  def self.source_root
10
14
  gem_root
11
15
  end
@@ -73,19 +77,15 @@ module InlineFormsInstaller
73
77
  exit 1
74
78
  end
75
79
 
76
- ruby_version = nil
80
+ target_ruby = InlineFormsInstaller::TARGET_RUBY_VERSION
77
81
  require "rvm"
78
82
  if RVM.current && !options[:skiprvm]
79
83
  say "Installing inline_forms with RVM", :green
80
- ruby_version = (%x[rvm current]).gsub(/@.*/, "")
81
- create_file "#{app_name}/.ruby-version", ruby_version
82
- create_file "#{app_name}/.ruby-gemset", app_name
83
84
  else
84
85
  say "Installing inline_forms without RVM", :green
85
86
  end
86
87
 
87
88
  say "Installing with #{options[:database]}", :green
88
- empty_directory(app_name)
89
89
 
90
90
  options.each do |k, v|
91
91
  ENV[k] = v.to_s
@@ -94,7 +94,8 @@ module InlineFormsInstaller
94
94
  ENV["using_sqlite"] = using_sqlite?.to_s
95
95
  ENV["database"] = database
96
96
  ENV["install_example"] = install_example?.to_s
97
- ENV["ruby_version"] = ruby_version.to_s
97
+ ENV["ruby_version"] = target_ruby
98
+ ENV["inline_forms_rvm_gemset"] = app_name if RVM.current && !options[:skiprvm]
98
99
  ENV["inline_forms_version"] = inline_forms_version
99
100
  ENV["inline_forms_installer_version"] = InlineFormsInstaller::VERSION
100
101
  ENV["INLINE_FORMS_INSTALLER_ROOT"] = InlineFormsInstaller.gem_root
@@ -108,7 +109,7 @@ module InlineFormsInstaller
108
109
  Gem::Specification
109
110
  .find_all_by_name("rails")
110
111
  .map(&:version)
111
- .select { |v| v >= Gem::Version.new("7.1") && v < Gem::Version.new("7.2") }
112
+ .select { |v| v >= Gem::Version.new("7.2") && v < Gem::Version.new("7.3") }
112
113
  .max
113
114
  rescue StandardError
114
115
  nil
@@ -1,30 +1,27 @@
1
1
  INSTALLER_ROOT = File.expand_path(ENV.fetch("INLINE_FORMS_INSTALLER_ROOT", File.expand_path("..", __dir__)))
2
2
  INLINE_FORMS_ROOT = File.expand_path(ENV.fetch("INLINE_FORMS_ROOT", INSTALLER_ROOT))
3
3
 
4
+ # Pin Ruby for the generated app (after `rails new`; do not write these files in
5
+ # Creator before `rails new` — Rails also emits `.ruby-version` and prompts).
6
+ create_file ".ruby-version", "#{ENV.fetch('ruby_version', 'ruby-4.0.4')}\n"
7
+ if (gemset = ENV["inline_forms_rvm_gemset"]).to_s != ""
8
+ create_file ".ruby-gemset", "#{gemset}\n"
9
+ end
10
+
4
11
  # Rails 7 dropped --skip-gemfile, so `rails new` always writes its own Gemfile.
5
12
  # Remove it so our `create_file` below does not prompt for overwrite.
6
13
  remove_file 'Gemfile' if File.exist?('Gemfile')
7
14
  create_file 'Gemfile', "# created by inline_forms #{ENV['inline_forms_version']} on #{Date.today}\n"
8
15
 
9
16
  # `rails new` is invoked with whatever the system `rails` binary points at
10
- # (often Rails 8.x once it lands in the global gemset), so the generated
11
- # `config/application.rb` may carry Rails 7.1+/8.0 idioms (`load_defaults
12
- # 8.0`, `config.autoload_lib(...)`). The Gemfile we write below pins
13
- # `rails ~> 7.0.0`, so Rails 7.0 must be able to interpret application.rb;
14
- # otherwise the first `bundle exec rails …` aborts with `Unknown version
15
- # "8.0"` or `NoMethodError: undefined method 'autoload_lib'`.
17
+ # (often Rails 8.x), so the generated `config/application.rb` may carry
18
+ # `load_defaults 8.0` and other 8.x-only settings. The Gemfile below pins
19
+ # `rails ~> 7.2.3`; normalize application.rb so the first `bundle exec rails`
20
+ # boot matches that pin.
16
21
  if File.exist?('config/application.rb')
17
22
  gsub_file 'config/application.rb',
18
23
  /config\.load_defaults\s+\d+\.\d+/,
19
- 'config.load_defaults 7.1'
20
- # Strip Rails 7.1+ `config.autoload_lib(ignore: ...)` (and any surrounding
21
- # explanatory comment block). Not supported on Rails 7.0.
22
- gsub_file 'config/application.rb',
23
- /^\s*#[^\n]*\n(\s*#[^\n]*\n)*\s*config\.autoload_lib\([^)]*\)\s*\n/,
24
- ""
25
- gsub_file 'config/application.rb',
26
- /^\s*config\.autoload_lib\([^)]*\)\s*\n/,
27
- ""
24
+ 'config.load_defaults 7.2'
28
25
  end
29
26
 
30
27
  add_source 'https://rubygems.org'
@@ -37,13 +34,13 @@ gem 'autoprefixer-rails'
37
34
  # foundation-rails 6.7+ uses Dart Sass (`sass:math`); sass-rails/sassc removed.
38
35
  # Visually tuned against foundation-rails ~> 6.6.2; current pin ~> 6.9 (6.9.0.x).
39
36
  gem 'foundation-rails', '~> 6.9'
40
- gem 'i18n-active_record', :git => 'https://github.com/acesuares/i18n-active_record.git'
41
- # Pin to the inline_forms version bundled with this installer release.
42
- # Set INLINE_FORMS_GEMFILE_PATH for maintainer local-path overrides only.
37
+ # Pin inline_forms and validation_hints on the 7.x line; Bundler resolves the
38
+ # highest 7.x that satisfies all deps. Set INLINE_FORMS_GEMFILE_PATH for
39
+ # maintainer local-path overrides only.
43
40
  if ENV["INLINE_FORMS_GEMFILE_PATH"] && File.directory?(ENV["INLINE_FORMS_GEMFILE_PATH"])
44
41
  gem "inline_forms", path: ENV["INLINE_FORMS_GEMFILE_PATH"]
45
42
  else
46
- gem "inline_forms", "~> #{ENV['inline_forms_version']}"
43
+ gem "inline_forms", "~> 7"
47
44
  end
48
45
  gem 'jquery-rails'
49
46
  gem 'jquery-timepicker-rails'
@@ -58,7 +55,7 @@ gem 'mysql2'
58
55
  gem 'paper_trail', '~> 16.0'
59
56
  gem 'rails-i18n', '~> 7.0'
60
57
  gem 'rails-jquery-autocomplete'
61
- gem 'rails', '~> 7.1.5'
58
+ gem 'rails', '~> 7.2.3'
62
59
  gem 'rake'
63
60
  gem 'rvm'
64
61
  gem 'dartsass-rails'
@@ -72,10 +69,10 @@ gem 'importmap-rails'
72
69
  # use `<turbo-frame>` + HTML responses (see docs/ujs-to-turbo.md). Registers the
73
70
  # `turbo_stream` MIME type for optional stream responses.
74
71
  gem 'turbo-rails'
75
- gem 'tabs_on_rails', :git => 'https://github.com/acesuares/tabs_on_rails.git', :branch => 'update_remote_before_action'
72
+ gem 'tabs_on_rails', '~> 3.0'
76
73
  gem 'unicorn'
77
- gem 'validation_hints', '~> 6.3'
78
- gem 'will_paginate' #, git: 'https://github.com/acesuares/will_paginate.git'
74
+ gem 'validation_hints', '~> 7'
75
+ gem 'will_paginate'
79
76
 
80
77
  gem_group :test do
81
78
  # Rails 7 still expects Minitest 5; 6.x breaks the railties test runner.
@@ -88,6 +85,7 @@ gem_group :development do
88
85
  gem 'capistrano', require: false
89
86
  gem 'capistrano3-unicorn'
90
87
  gem 'listen'
88
+ gem 'puma', '>= 5.0'
91
89
  gem 'rvm-capistrano', :require => false
92
90
  gem 'rvm1-capistrano3', require: false
93
91
  gem 'seed_dump', '~> 0.5.3'
@@ -230,7 +228,7 @@ create_file "db/migrate/" +
230
228
  Time.now.utc.strftime("%Y%m%d%H%M%S") +
231
229
  "_" +
232
230
  "devise_create_users.rb", <<-DEVISE_MIGRATION.strip_heredoc
233
- class DeviseCreateUsers < ActiveRecord::Migration[7.1]
231
+ class DeviseCreateUsers < ActiveRecord::Migration[7.2]
234
232
 
235
233
  def change
236
234
  create_table(:users) do |t|
@@ -317,7 +315,7 @@ create_file "app/models/user.rb", <<-USER_MODEL.strip_heredoc
317
315
  attr_reader :per_page
318
316
  @per_page = 7
319
317
 
320
- has_paper_trail
318
+ has_paper_trail on: [:create, :update, :destroy]
321
319
 
322
320
  def _presentation
323
321
  "\#{name}"
@@ -383,7 +381,7 @@ create_file "db/migrate/" +
383
381
  Time.now.utc.strftime("%Y%m%d%H%M%S") +
384
382
  "_" +
385
383
  "inline_forms_create_join_table_user_role.rb", <<-ROLES_MIGRATION.strip_heredoc
386
- class InlineFormsCreateJoinTableUserRole < ActiveRecord::Migration[7.1]
384
+ class InlineFormsCreateJoinTableUserRole < ActiveRecord::Migration[7.2]
387
385
  def self.up
388
386
  create_table :roles_users, :id => false, :force => true do |t|
389
387
  t.integer :role_id
@@ -439,8 +437,12 @@ say "- Track ActionText (rich_text) edits with PaperTrail..."
439
437
  # `ActionText::RichText` in the generated app.
440
438
  create_file 'config/initializers/rich_text_paper_trail.rb', <<-PT_RICH_TEXT.strip_heredoc
441
439
  # Generated by inline_forms.
440
+ # Mirror the per-model opt-out from `:touch` (see app/models/*.rb): nothing
441
+ # touches ActionText::RichText today, but if a future association adds
442
+ # `touch: true` pointing at it, the parent versions panel must not surface
443
+ # touch-only "empty" rich-text rows whose Restore link reifies a no-op.
442
444
  ActiveSupport.on_load(:action_text_rich_text) do
443
- has_paper_trail
445
+ has_paper_trail on: [:create, :update, :destroy]
444
446
  end
445
447
  PT_RICH_TEXT
446
448
 
@@ -482,7 +484,7 @@ create_file "db/migrate/" +
482
484
  Time.now.utc.strftime("%Y%m%d%H%M%S") +
483
485
  "_" +
484
486
  "inline_forms_create_view_for_translations.rb", <<-VIEW_MIGRATION.strip_heredoc
485
- class InlineFormsCreateViewForTranslations < ActiveRecord::Migration[7.1]
487
+ class InlineFormsCreateViewForTranslations < ActiveRecord::Migration[7.2]
486
488
  def self.up
487
489
  execute 'CREATE VIEW translations
488
490
  AS
@@ -697,7 +699,7 @@ if ENV['install_example'] == 'true'
697
699
  say "- Apartment name is required..."
698
700
  inject_into_file "app/models/apartment.rb",
699
701
  "\n validates :name, presence: true\n",
700
- after: " has_paper_trail\n"
702
+ after: " has_paper_trail on: [:create, :update, :destroy]\n"
701
703
 
702
704
  # CarrierWave + PaperTrail history.
703
705
  # PaperTrail snapshots the column scalar (the stored filename) on update,
@@ -807,51 +809,9 @@ if ENV['install_example'] == 'true'
807
809
  copy_file abs, File.join("db/seed_images", File.basename(abs))
808
810
  end
809
811
 
810
- say "- Generating Konferensha apartment + photos seed migration..."
811
- sleep 1 # unique migration timestamp
812
- seed_ts = Time.now.utc.strftime("%Y%m%d%H%M%S")
813
- create_file "db/migrate/#{seed_ts}_seed_konferensha_photos.rb", <<-SEED_MIGRATION.strip_heredoc
814
- class SeedKonferenshaPhotos < ActiveRecord::Migration[7.1]
815
- # Seed an Apartment with a gallery of photos so the nested
816
- # has_many list (apartments -> photos) has enough rows to
817
- # trigger pagination. Driven by db/seed_images/, which the
818
- # inline_forms installer copies from the gem's pics/ dir.
819
- # Runs in development (via db:migrate) and against the test
820
- # DB (via db:test:prepare), so integration tests can assert
821
- # the paginated <turbo-frame> renders without seeding manually.
822
- def up
823
- apartment = Apartment.find_or_create_by!(name: "Konferensha") do |a|
824
- a.title = "Konferensha sobre Papiamentu"
825
- a.opening_date = Date.new(2020, 5, 18)
826
- end
827
-
828
- seed_dir = Rails.root.join("db", "seed_images")
829
- return unless seed_dir.directory?
830
-
831
- Dir.glob(seed_dir.join("*.{jpg,jpeg,png,gif}"), File::FNM_CASEFOLD).sort.each do |abs|
832
- base = File.basename(abs)
833
- next if Photo.exists?(name: base, apartment_id: apartment.id)
834
- File.open(abs, "rb") do |io|
835
- Photo.create!(
836
- name: base,
837
- caption: "Konferensha foto \#{base}",
838
- apartment: apartment,
839
- image: io
840
- )
841
- end
842
- end
843
- end
844
-
845
- def down
846
- apartment = Apartment.find_by(name: "Konferensha")
847
- return unless apartment
848
- apartment.photos.destroy_all
849
- apartment.destroy
850
- end
851
- end
852
- SEED_MIGRATION
853
-
854
- run "bundle exec rake db:migrate"
812
+ # The actual seed migration for 3 apartments + 3 owners + photos
813
+ # is generated AFTER the Owner setup below (so it can reference
814
+ # owners + apartments.owner_id). We just copy the seed pics here.
855
815
  end
856
816
  end
857
817
 
@@ -862,6 +822,213 @@ if ENV['install_example'] == 'true'
862
822
  "\n skip_load_and_authorize_resource only: :name_list\n\n def name_list\n authorize! :read, Apartment\n @apartments = Apartment.accessible_by(current_ability).order(:id).limit(10)\n end\n",
863
823
  after: "set_tab :apartment\n"
864
824
 
825
+ # Owner -- demonstrates per-resource sub-tabs on /owners/:id.
826
+ # Owner has many Apartments; an Apartment belongs to one Owner. The
827
+ # Owner detail panel renders two Turbo tabs (`naw`, `apartments`) via
828
+ # InlineForms::TurboTabsBuilder; both tabs surface :name, hence the
829
+ # shared first field. See OwnersController override below.
830
+ say "- Generating Owner model (has_many apartments)..."
831
+ sleep 1
832
+ run %q{bundle exec rails g inline_forms Owner name:string birthdate:date address:string city:string country:string apartments:has_many apartments:associated _enabled:yes _presentation:'#{name}'}
833
+
834
+ say "- Owner name is required..."
835
+ inject_into_file "app/models/owner.rb",
836
+ "\n validates :name, presence: true\n",
837
+ after: " has_paper_trail on: [:create, :update, :destroy]\n"
838
+
839
+ say "- Adding owner_id to apartments + belongs_to :owner..."
840
+ sleep 1
841
+ add_owner_ts = Time.now.utc.strftime("%Y%m%d%H%M%S")
842
+ create_file "db/migrate/#{add_owner_ts}_add_owner_to_apartments.rb", <<-ADD_OWNER.strip_heredoc
843
+ class AddOwnerToApartments < ActiveRecord::Migration[7.2]
844
+ def change
845
+ add_reference :apartments, :owner, null: true, foreign_key: true
846
+ end
847
+ end
848
+ ADD_OWNER
849
+
850
+ inject_into_file "app/models/apartment.rb",
851
+ " belongs_to :owner, optional: true\n",
852
+ after: " has_paper_trail on: [:create, :update, :destroy]\n"
853
+
854
+ # Insert the :owner dropdown row at the top of Apartment's attribute list
855
+ # so it appears above :name in the inline panel.
856
+ gsub_file "app/models/apartment.rb",
857
+ /@inline_forms_attribute_list \|\|= \[\n/,
858
+ "@inline_forms_attribute_list ||= [\n [ :owner , \"owner\", :dropdown ], \n"
859
+
860
+ # Owner -> apartments: render as a check_list of EXISTING apartments
861
+ # (not the default :associated panel that only lets you create new
862
+ # rows nested under the owner). Standard Rails has_many gives us the
863
+ # `apartment_ids=` setter that CheckListHelper uses, so we just swap
864
+ # the form element kind in the generated attribute list.
865
+ gsub_file "app/models/owner.rb",
866
+ /\[ :apartments , "apartments", :associated \]/,
867
+ '[ :apartments , "apartments", :check_list ]'
868
+
869
+ say "- Replacing OwnersController with tabbed-show variant (/owners/:id)..."
870
+ remove_file "app/controllers/owners_controller.rb"
871
+ create_file "app/controllers/owners_controller.rb", <<-OWNERS_CTRL.strip_heredoc
872
+ class OwnersController < InlineFormsController
873
+ set_tab :owner
874
+
875
+ # Per-owner sub-tabs. `name` appears on both tabs by design (the user
876
+ # asked for `name + apartments` on one tab and `naw` -- name,
877
+ # birthdate, address, city, country -- on the other).
878
+ OWNER_TABS = %w[naw apartments].freeze
879
+ OWNER_TAB_FIELDS = {
880
+ "naw" => %i[name birthdate address city country],
881
+ "apartments" => %i[name apartments],
882
+ }.freeze
883
+
884
+ def show
885
+ # Field-level inline edit / cancel / explicit close requests
886
+ # still go through the stock `_show` / field flows.
887
+ return super if params[:form_element] || params[:attribute] || params[:close]
888
+
889
+ @object = Owner.find(params[:id])
890
+ @update_span = params[:update].presence || "owner_\#{@object.id}"
891
+
892
+ tab = OWNER_TABS.include?(params[:tab].to_s) ? params[:tab].to_s : "naw"
893
+ set_tab tab.to_sym
894
+ @inline_forms_owner_tabs = OWNER_TABS
895
+ @inline_forms_attribute_list = owner_attributes_for(tab)
896
+ @inline_forms_turbo_row = true
897
+
898
+ render "owners/show_with_tabs",
899
+ layout: turbo_frame_request? ? "turbo_rails/frame" : "inline_forms"
900
+ end
901
+
902
+ private
903
+
904
+ def owner_attributes_for(tab)
905
+ full = @object.inline_forms_attribute_list
906
+ OWNER_TAB_FIELDS.fetch(tab).map do |attr|
907
+ full.find { |a, _, _| a == attr } ||
908
+ raise("OwnersController: attribute \#{attr.inspect} missing from Owner#inline_forms_attribute_list")
909
+ end
910
+ end
911
+ end
912
+ OWNERS_CTRL
913
+
914
+ # Seed the example app with 3 apartments + 3 owners and assign photos
915
+ # from db/seed_images. Runs as a regular migration so `bundle exec rake
916
+ # db:migrate` (and `rails db:setup` on a fresh checkout) populates the
917
+ # demo gallery in one shot. Idempotent via find_or_create_by!.
918
+ say "- Generating seed migration (3 apartments + 3 owners + photo gallery)..."
919
+ sleep 1
920
+ seed_ts = Time.now.utc.strftime("%Y%m%d%H%M%S")
921
+ create_file "db/migrate/#{seed_ts}_seed_example_apartments_and_owners.rb", <<-SEED_MIGRATION.strip_heredoc
922
+ class SeedExampleApartmentsAndOwners < ActiveRecord::Migration[7.2]
923
+ # ---------------------------------------------------------------
924
+ # Apartment seed gallery
925
+ # ---------------------------------------------------------------
926
+ # Three apartments named "Apt 1", "Apt 2", "Apt 3". Each apartment
927
+ # gets the photos under db/seed_images/ that start with apt<N>_,
928
+ # falling back to a slice of the directory when no per-apartment
929
+ # files match. The default seed_images shipped with the gem are
930
+ # CC0 placeholders generated by the installer (solid pastel +
931
+ # apartment label), so users can fork and replace freely.
932
+ APARTMENTS = [
933
+ { name: "Apt 1", title: "Casa Aurora", opening_date: Date.new(2026, 1, 10) },
934
+ { name: "Apt 2", title: "Villa Marina", opening_date: Date.new(2026, 2, 14) },
935
+ { name: "Apt 3", title: "Loft del Sol", opening_date: Date.new(2026, 3, 21) },
936
+ ].freeze
937
+
938
+ # ---------------------------------------------------------------
939
+ # Owners
940
+ # ---------------------------------------------------------------
941
+ # Maria owns Apt 1 + Apt 2 (the "many" case), Jean-Pierre owns
942
+ # exactly one (Apt 3), and Akira owns zero apartments so the
943
+ # check_list edit panel on /owners/:id can be exercised against
944
+ # an empty association too.
945
+ OWNERS = [
946
+ { name: "Maria Martinez",
947
+ birthdate: Date.new(1984, 7, 12),
948
+ address: "Calle del Sol 42",
949
+ city: "Willemstad",
950
+ country: "Curacao",
951
+ apartments: ["Apt 1", "Apt 2"] },
952
+ { name: "Jean-Pierre Dupont",
953
+ birthdate: Date.new(1972, 3, 4),
954
+ address: "Rue des Lilas 7",
955
+ city: "Lyon",
956
+ country: "France",
957
+ apartments: ["Apt 3"] },
958
+ { name: "Akira Tanaka",
959
+ birthdate: Date.new(1990, 11, 23),
960
+ address: "1-2-3 Sakura",
961
+ city: "Kyoto",
962
+ country: "Japan",
963
+ apartments: [] },
964
+ ].freeze
965
+
966
+ def up
967
+ seed_dir = Rails.root.join("db", "seed_images")
968
+ all_pics = seed_dir.directory? ?
969
+ Dir.glob(seed_dir.join("*.{png,jpg,jpeg,gif}"), File::FNM_CASEFOLD).sort :
970
+ []
971
+
972
+ apt_records = {}
973
+ APARTMENTS.each_with_index do |spec, idx|
974
+ apt = Apartment.find_or_create_by!(name: spec[:name]) do |a|
975
+ a.title = spec[:title]
976
+ a.opening_date = spec[:opening_date]
977
+ end
978
+
979
+ prefix = "apt\#{idx + 1}_"
980
+ per_apt = all_pics.select { |abs| File.basename(abs).downcase.start_with?(prefix) }
981
+ per_apt = all_pics.each_slice(3).to_a[idx].to_a if per_apt.empty? && all_pics.any?
982
+
983
+ per_apt.each do |abs|
984
+ base = File.basename(abs)
985
+ next if Photo.exists?(name: base, apartment_id: apt.id)
986
+ File.open(abs, "rb") do |io|
987
+ Photo.create!(
988
+ name: base,
989
+ caption: "\#{spec[:title]} -- \#{base}",
990
+ apartment: apt,
991
+ image: io
992
+ )
993
+ end
994
+ end
995
+
996
+ apt_records[spec[:name]] = apt
997
+ end
998
+
999
+ OWNERS.each do |spec|
1000
+ owner = Owner.find_or_create_by!(name: spec[:name]) do |o|
1001
+ o.birthdate = spec[:birthdate]
1002
+ o.address = spec[:address]
1003
+ o.city = spec[:city]
1004
+ o.country = spec[:country]
1005
+ end
1006
+
1007
+ spec[:apartments].each do |apt_name|
1008
+ apt = apt_records[apt_name] or next
1009
+ apt.update!(owner: owner) unless apt.owner_id == owner.id
1010
+ end
1011
+ end
1012
+ end
1013
+
1014
+ def down
1015
+ OWNERS.each do |spec|
1016
+ owner = Owner.find_by(name: spec[:name])
1017
+ owner&.destroy
1018
+ end
1019
+ APARTMENTS.each do |spec|
1020
+ apt = Apartment.find_by(name: spec[:name])
1021
+ next unless apt
1022
+ apt.photos.destroy_all
1023
+ apt.destroy
1024
+ end
1025
+ end
1026
+ end
1027
+ SEED_MIGRATION
1028
+
1029
+ say "- Running migrations for owner + seed (owners + apartments.owner_id + 3 apts/3 owners)..."
1030
+ run "bundle exec rake db:migrate"
1031
+
865
1032
  example_views_root = File.join(INSTALLER_ROOT, "lib/installer_templates/example_app_views")
866
1033
  Dir.glob(File.join(example_views_root, "**", "*")).sort.each do |abs|
867
1034
  next unless File.file?(abs)
@@ -879,10 +1046,11 @@ if ENV['install_example'] == 'true'
879
1046
  create_file rel, File.read(abs)
880
1047
  end
881
1048
 
882
- say "\nDone! Example app (Photo + Apartment) is ready.", :yellow
1049
+ say "\nDone! Example app (Photo + Apartment + Owner) is ready.", :yellow
883
1050
  say " bundle exec rails test # example regression tests", :yellow
884
1051
  say " bundle exec rails s # then http://localhost:3000/apartments", :yellow
885
1052
  say " More menu → Apartment names (first 10) # /apartments/name_list", :yellow
1053
+ say " More menu → Owners # /owners (per-owner 2 tabs)", :yellow
886
1054
  say " Log in: #{ENV["email"]} / #{ENV["password"]}", :yellow
887
1055
  end
888
1056
  # done!
@@ -1,6 +1,9 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module InlineFormsInstaller
3
- VERSION = "7.11.0"
3
+ VERSION = "7.13.11"
4
+
5
+ # Written into generated apps' `.ruby-version` (must match gemspec `required_ruby_version`).
6
+ TARGET_RUBY_VERSION = "ruby-4.0.4"
4
7
 
5
8
  # Kept in sync with inline_forms gem releases from the same repo tag.
6
9
  INLINE_FORMS_VERSION = VERSION
@@ -309,12 +309,13 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
309
309
  assert_includes @response.body, %(<turbo-frame id="#{frame_id}">)
310
310
 
311
311
  seed_dir = Rails.root.join("db", "seed_images")
312
- jpgs = Dir.glob(seed_dir.join("*.{jpg,jpeg}"), File::FNM_CASEFOLD).sort
313
- assert_operator jpgs.size, :>=, 2,
314
- "need at least two seed jpgs so replacement can differ from current mount"
312
+ seeds = Dir.glob(seed_dir.join("*.{jpg,jpeg,png,gif}"), File::FNM_CASEFOLD).sort
313
+ assert_operator seeds.size, :>=, 2,
314
+ "need at least two seed images so replacement can differ from current mount"
315
315
 
316
- replacement = jpgs.find { |abs| File.basename(abs) != photo.name } || jpgs.last
317
- uploaded = Rack::Test::UploadedFile.new(replacement, "image/jpeg")
316
+ replacement = seeds.find { |abs| File.basename(abs) != photo.name } || seeds.last
317
+ mime = (replacement.to_s.downcase.end_with?(".png") ? "image/png" : "image/jpeg")
318
+ uploaded = Rack::Test::UploadedFile.new(replacement, mime)
318
319
 
319
320
  put photo_path(
320
321
  photo,
@@ -328,7 +329,7 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
328
329
  assert_response :success,
329
330
  "multipart image update must respond with HTML (not 406 UnknownFormat)"
330
331
  assert_includes @response.body, %(<turbo-frame id="#{frame_id}">)
331
- refute_match(/UnknownFormat|406/, @response.body)
332
+ refute_match(/UnknownFormat|406 Not Acceptable/, @response.body)
332
333
 
333
334
  photo.reload
334
335
  assert photo.image.present?, "expected CarrierWave mount after Turbo multipart PUT"
@@ -346,9 +347,10 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
346
347
  turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
347
348
 
348
349
  seed_dir = Rails.root.join("db", "seed_images")
349
- jpgs = Dir.glob(seed_dir.join("*.{jpg,jpeg}"), File::FNM_CASEFOLD).sort
350
- replacement = jpgs.find { |abs| File.basename(abs) != photo.name } || jpgs.last
351
- uploaded = Rack::Test::UploadedFile.new(replacement, "image/jpeg")
350
+ seeds = Dir.glob(seed_dir.join("*.{jpg,jpeg,png,gif}"), File::FNM_CASEFOLD).sort
351
+ replacement = seeds.find { |abs| File.basename(abs) != photo.name } || seeds.last
352
+ mime = (replacement.to_s.downcase.end_with?(".png") ? "image/png" : "image/jpeg")
353
+ uploaded = Rack::Test::UploadedFile.new(replacement, mime)
352
354
 
353
355
  put photo_path(
354
356
  photo,
@@ -416,8 +418,12 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
416
418
  assert_match %r{<turbo-frame id="#{frame}"}, @response.body
417
419
  assert_match %r{<turbo-frame id="#{@update_span}"}, @response.body
418
420
 
419
- seed = Rails.root.join("db/seed_images/dsc00099.jpg")
420
- uploaded = Rack::Test::UploadedFile.new(seed, "image/jpeg")
421
+ seeds = Dir.glob(Rails.root.join("db/seed_images/*.{jpg,jpeg,png,gif}"),
422
+ File::FNM_CASEFOLD).sort
423
+ seed = seeds.last
424
+ raise "no seed images in db/seed_images/" unless seed
425
+ mime = (seed.to_s.downcase.end_with?(".png") ? "image/png" : "image/jpeg")
426
+ uploaded = Rack::Test::UploadedFile.new(seed, mime)
421
427
 
422
428
  assert_difference("Photo.count", 1) do
423
429
  post photos_path(
@@ -435,6 +441,10 @@ class ExampleAppApartmentPhotosPaginationTest < ExampleAppIntegrationTestCase
435
441
  assert_response :success
436
442
  assert_match %r{<turbo-frame id="#{frame}"}, @response.body
437
443
  assert_match %r{<turbo-frame id="#{@update_span}"}, @response.body
438
- assert_includes @response.body, "curl_new_photo.jpg"
444
+ # The new row may be on a later pagination page (Photo.per_page = 5),
445
+ # so don't rely on it being in the first-page list HTML; assert on the
446
+ # DB instead -- this is exactly the Photo we just POSTed.
447
+ assert Photo.exists?(name: "curl_new_photo.jpg", apartment_id: @apartment.id),
448
+ "expected create POST to persist the new Photo under @apartment"
439
449
  end
440
450
  end
@@ -117,4 +117,162 @@ class ExampleAppApartmentVersionsTurboTest < ExampleAppIntegrationTestCase
117
117
  assert_includes apt.description.body.to_html, "old body",
118
118
  "rich_text revert should restore the previous body content"
119
119
  end
120
+
121
+ # PaperTrail::Version#reify returns nil for `create` events (no prior state).
122
+ # For ActionText::RichText we treat reverting a `create` as "undo the
123
+ # creation": destroy the rich_text record so the parent's field reverts
124
+ # to empty. This keeps Restore symmetric regardless of whether the
125
+ # field's first rich_text save happened to be empty (reverting v2 update
126
+ # returns to empty) or already had content (reverting v1 create destroys
127
+ # the row = empty). The user-reported asymmetry was: Apartment's first
128
+ # rich_text save was empty in the example data so Restore visibly
129
+ # cleared the field via an update revert; nested Photo's first
130
+ # rich_text save had content so the create was the only Restore target
131
+ # and used to be hidden / a no-op.
132
+ test "revert on rich_text create destroys the rich_text record so the field becomes empty" do
133
+ apt = Apartment.create!(name: "RichText Create Revert", title: "T")
134
+ apt.update!(description: "<p>only body</p>")
135
+
136
+ rich_text = ActionText::RichText.find_by!(
137
+ record_type: Apartment.name, record_id: apt.id, name: "description"
138
+ )
139
+ create_version = rich_text.versions.where(event: "create").order(:id).first
140
+ assert create_version, "expected a PaperTrail create version on the rich_text record"
141
+
142
+ row_frame = "apartment_#{apt.id}"
143
+ versions_frame = "#{row_frame}_versions"
144
+ post revert_apartment_path(create_version.id, update: row_frame),
145
+ headers: {
146
+ "Turbo-Frame" => versions_frame,
147
+ "Accept" => "text/vnd.turbo-stream.html"
148
+ }
149
+ assert_response :success
150
+ assert_includes @response.body, %(action="replace")
151
+ assert_includes @response.body, %(target="#{row_frame}")
152
+ assert_includes @response.body, %(target="#{versions_frame}")
153
+
154
+ apt.reload
155
+ assert_not apt.description.body.present?,
156
+ "reverting a rich_text create must clear the field"
157
+ refute ActionText::RichText.exists?(
158
+ record_type: Apartment.name, record_id: apt.id, name: "description"
159
+ ), "the underlying ActionText::RichText row must be destroyed by the revert"
160
+ end
161
+
162
+ test "revert on nested Photo rich_text create destroys the rich_text record" do
163
+ apt = Apartment.create!(name: "Photo RT Create Revert", title: "T")
164
+ photo = apt.photos.create!(name: "p.jpg", caption: "x")
165
+ photo.update!(description: "<p>photo body</p>")
166
+
167
+ rich_text = ActionText::RichText.find_by!(
168
+ record_type: Photo.name, record_id: photo.id, name: "description"
169
+ )
170
+ create_version = rich_text.versions.where(event: "create").order(:id).first
171
+ assert create_version,
172
+ "expected a PaperTrail create version on the Photo's description rich_text"
173
+
174
+ row_frame = "apartment_#{apt.id}_photo_#{photo.id}"
175
+ versions_frame = "#{row_frame}_versions"
176
+ post revert_photo_path(create_version.id, update: row_frame),
177
+ headers: {
178
+ "Turbo-Frame" => versions_frame,
179
+ "Accept" => "text/vnd.turbo-stream.html"
180
+ }
181
+ assert_response :success
182
+ assert_includes @response.body, %(target="#{row_frame}")
183
+ assert_includes @response.body, %(target="#{versions_frame}")
184
+
185
+ photo.reload
186
+ assert_not photo.description.body.present?,
187
+ "reverting a Photo rich_text create must clear the description"
188
+ refute ActionText::RichText.exists?(
189
+ record_type: Photo.name, record_id: photo.id, name: "description"
190
+ ), "the Photo's ActionText::RichText row must be destroyed by the revert"
191
+ end
192
+
193
+ # Pair with the model template change in `lib/generators/templates/model.erb`
194
+ # (`has_paper_trail on: [:create, :update, :destroy]`): PaperTrail 16 tracks
195
+ # `:touch` by default, and ActionText's `belongs_to :record, touch: true`
196
+ # fires `parent.touch` on every rich-text save, producing parent-side
197
+ # `update` versions with `changeset == {}`. The versions panel reified
198
+ # those to the same state (no-op Restore). Opt out of `:touch` and no such
199
+ # row appears.
200
+ test "creating a record with a rich_text body does not append a touch-only parent update" do
201
+ apt = Apartment.create!(name: "Touch Free", title: "T", description: "<p>seed</p>")
202
+ update_events_with_nothing_to_replay = apt.versions.where(event: "update").select do |v|
203
+ v.changeset.nil? || v.changeset.except("updated_at").empty?
204
+ end
205
+ assert_empty update_events_with_nothing_to_replay,
206
+ "Apartment should not gain a touch-driven empty-changeset update version on rich_text save"
207
+ end
208
+
209
+ # Defensive view-level guard (covers legacy apps still tracking :touch and
210
+ # any other empty-update source — e.g. CarrierWave callback flips that
211
+ # change nothing user-visible).
212
+ test "versions list hides Restore link on empty-changeset update rows" do
213
+ apt = Apartment.create!(name: "Empty Update Hidden", title: "T")
214
+ # Simulate a touch-driven update version (legacy `has_paper_trail`
215
+ # without `on:` filter, or any future :touch source).
216
+ PaperTrail::Version.create!(
217
+ item_type: apt.class.name,
218
+ item_id: apt.id,
219
+ event: "update",
220
+ whodunnit: "system",
221
+ object: nil,
222
+ object_changes: nil,
223
+ created_at: Time.current
224
+ )
225
+ empty_v = PaperTrail::Version.where(item_type: apt.class.name, item_id: apt.id, event: "update").last
226
+ assert empty_v.changeset.nil? || empty_v.changeset.except("updated_at").empty?
227
+
228
+ vf = "apartment_#{apt.id}_versions"
229
+ get list_versions_apartment_path(apt, update: vf),
230
+ headers: { "Turbo-Frame" => vf, "Accept" => "text/html" }
231
+ assert_response :success
232
+ refute_includes @response.body, "/apartments/#{empty_v.id}/revert",
233
+ "Restore link must be hidden for empty-changeset update versions"
234
+ end
235
+
236
+ # Primary (`:kind == :primary`) create rows still hide their Restore link:
237
+ # reverting a primary create would mean destroying the record itself,
238
+ # which is what the Destroy button is for. Only rich_text creates get a
239
+ # Restore link (it destroys just the ActionText::RichText row).
240
+ test "versions list hides Restore link on primary create rows but keeps it on update rows" do
241
+ apt = Apartment.create!(name: "Create Link Hidden", title: "Before")
242
+ apt.update!(title: "After")
243
+ vf = "apartment_#{apt.id}_versions"
244
+ get list_versions_apartment_path(apt, update: vf),
245
+ headers: { "Turbo-Frame" => vf, "Accept" => "text/html" }
246
+ assert_response :success
247
+
248
+ update_v = apt.versions.where(event: "update").order(:id).last
249
+ create_v = apt.versions.where(event: "create").order(:id).first
250
+ assert update_v && create_v, "expected both create and update versions on the parent"
251
+
252
+ assert_includes @response.body, "/apartments/#{update_v.id}/revert",
253
+ "Restore link must remain for update versions"
254
+ refute_includes @response.body, "/apartments/#{create_v.id}/revert",
255
+ "Restore link must be hidden for primary create versions (reify nil; Destroy is the user-facing action)"
256
+ end
257
+
258
+ # Conversely, rich_text create rows MUST keep their Restore link — that
259
+ # is the only way to undo "I added content" when the field was created
260
+ # in one save with non-empty content (the originally reported asymmetry
261
+ # vs. fields whose first save happened to be empty).
262
+ test "versions list shows Restore link on rich_text create rows" do
263
+ apt = Apartment.create!(name: "RT Create Link Shown", title: "T")
264
+ apt.update!(description: "<p>seed body</p>")
265
+ rich_text = ActionText::RichText.find_by!(
266
+ record_type: Apartment.name, record_id: apt.id, name: "description"
267
+ )
268
+ rt_create_v = rich_text.versions.where(event: "create").order(:id).first
269
+ assert rt_create_v
270
+
271
+ vf = "apartment_#{apt.id}_versions"
272
+ get list_versions_apartment_path(apt, update: vf),
273
+ headers: { "Turbo-Frame" => vf, "Accept" => "text/html" }
274
+ assert_response :success
275
+ assert_includes @response.body, "/apartments/#{rt_create_v.id}/revert",
276
+ "Restore link must be present on rich_text create rows so the create can be undone"
277
+ end
120
278
  end
@@ -0,0 +1,135 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../example_app/example_integration_test_case"
4
+
5
+ # /owners/:id ships two Turbo sub-tabs (`naw`, `apartments`). The custom
6
+ # OwnersController#show picks one of two attribute subsets driven by
7
+ # `params[:tab]`, sets `set_tab` so the active tab is highlighted, and
8
+ # renders inside the row `<turbo-frame id="owner_<id>">` so a tab click
9
+ # is a single partial swap. `name` deliberately appears on both tabs.
10
+ class ExampleAppOwnerTabsTest < ExampleAppIntegrationTestCase
11
+ setup do
12
+ apartment = Apartment.first ||
13
+ Apartment.create!(name: "Owner Tabs Apt", title: "T")
14
+ @owner = Owner.create!(
15
+ name: "Tabs Owner #{SecureRandom.hex(3)}",
16
+ birthdate: Date.new(1980, 1, 2),
17
+ address: "1 Test St",
18
+ city: "Willemstad",
19
+ country: "Curaçao"
20
+ )
21
+ apartment.update!(owner: @owner)
22
+ @row_frame = "owner_#{@owner.id}"
23
+ @row_headers = { "Turbo-Frame" => @row_frame, "Accept" => "text/html" }
24
+ end
25
+
26
+ test "owners index wraps each row in a turbo-frame" do
27
+ get owners_path
28
+ assert_response :success
29
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
30
+ end
31
+
32
+ test "row open renders the tab strip + the default NAW tab" do
33
+ get owner_path(@owner, update: @row_frame), headers: @row_headers
34
+ assert_response :success
35
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
36
+
37
+ # Tab strip is present, both labels are visible.
38
+ assert_select "ul#owner_#{@owner.id}_tabs", count: 1
39
+ assert_select "ul#owner_#{@owner.id}_tabs li", count: 2
40
+
41
+ # NAW is the active tab on the default request. The active tab is a
42
+ # non-clickable `<a aria-current="page">` (so Foundation 6 styles it
43
+ # via `.tabs-title.is-active > a` / `[aria-selected="true"]`); the
44
+ # inactive tab is a real `<a>` carrying `data-turbo-frame="<row_frame>"`.
45
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a[aria-current=?]",
46
+ "page", text: /Naw/i, count: 1
47
+ assert_select "ul#owner_#{@owner.id}_tabs a[data-turbo-frame=?]",
48
+ @row_frame, minimum: 1
49
+ # And each tab `<li>` carries Foundation's `tabs-title` class.
50
+ assert_select "ul#owner_#{@owner.id}_tabs li.tabs-title", count: 2
51
+
52
+ # NAW attribute subset is rendered; the Apartments-tab-only field
53
+ # (the :apartments check_list, rendered inside the turbo-frame
54
+ # owner_<id>_apartments) is NOT present here.
55
+ assert_includes @response.body, "birthdate"
56
+ assert_includes @response.body, "country"
57
+ refute_match %r{<turbo-frame[^>]*id="owner_#{@owner.id}_apartments"},
58
+ @response.body
59
+ end
60
+
61
+ test "tab=apartments shows the apartments associated list and shared name" do
62
+ get owner_path(@owner, update: @row_frame, tab: "apartments"),
63
+ headers: @row_headers
64
+ assert_response :success
65
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
66
+
67
+ # Active tab is now "apartments".
68
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a[aria-current=?]",
69
+ "page", text: /Apartments/i, count: 1
70
+
71
+ # The `name` field is on BOTH tabs (shared field by design).
72
+ assert_includes @response.body, "name"
73
+
74
+ # Owner#apartments is now a :check_list (was :associated). _show.html.erb
75
+ # renders scalar/check_list rows inside a turbo-frame id'd
76
+ # "<model>_<id>_<attribute>" -- assert that frame is present so we know
77
+ # the apartments row landed on this tab.
78
+ assert_match %r{<turbo-frame[^>]*id="owner_#{@owner.id}_apartments"},
79
+ @response.body
80
+
81
+ # NAW-only fields (e.g. birthdate, country) are NOT rendered on this tab.
82
+ refute_match(/data-attribute="birthdate"/, @response.body)
83
+ refute_match(/data-attribute="country"/, @response.body)
84
+ end
85
+
86
+ test "unknown tab parameter falls back to NAW" do
87
+ get owner_path(@owner, update: @row_frame, tab: "bogus"),
88
+ headers: @row_headers
89
+ assert_response :success
90
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a[aria-current=?]",
91
+ "page", text: /Naw/i, count: 1
92
+ end
93
+
94
+ test "close link still uses stock controller flow (not tabbed render)" do
95
+ get owner_path(@owner, update: @row_frame, close: true),
96
+ headers: @row_headers
97
+ assert_response :success
98
+ assert_includes @response.body, %(<turbo-frame id="#{@row_frame}">)
99
+ # _close renders the collapsed row -- no tab strip, no presentation panel.
100
+ refute_select "ul#owner_#{@owner.id}_tabs"
101
+ refute_includes @response.body, "object_presentation"
102
+ end
103
+
104
+ test "tab links carry data-turbo-frame on the anchor (TurboTabsBuilder)" do
105
+ get owner_path(@owner, update: @row_frame), headers: @row_headers
106
+ assert_response :success
107
+
108
+ # The inactive tab's link must carry data-turbo-frame pointing at the
109
+ # row frame -- this is exactly what TurboTabsBuilder threads through
110
+ # to <a> via link_options. Upstream tabs_on_rails 3.0 could only
111
+ # annotate the <li>, so this assertion would fail without it.
112
+ assert_select(
113
+ "ul#owner_#{@owner.id}_tabs li:not(.is-active) a[data-turbo-frame=?]",
114
+ @row_frame,
115
+ minimum: 1
116
+ )
117
+ refute_select "ul#owner_#{@owner.id}_tabs a[data-remote='true']"
118
+ end
119
+
120
+ test "TurboTabsBuilder renders the active tab as an <a> without href" do
121
+ get owner_path(@owner, update: @row_frame, tab: "apartments"),
122
+ headers: @row_headers
123
+ assert_response :success
124
+ # Foundation 6's `.tabs-title.is-active > a` rule (and the
125
+ # `[aria-selected='true']` rule in _tabs.scss) only fires when the
126
+ # active label is itself an <a>. TurboTabsBuilder emits the active
127
+ # label as a hrefless <a aria-current="page" aria-selected="true">
128
+ # so the tab gets the framework's active styling without becoming
129
+ # clickable.
130
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a", count: 1
131
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a[href]", count: 0
132
+ assert_select "ul#owner_#{@owner.id}_tabs li.is-active a[aria-selected=?]",
133
+ "true", count: 1
134
+ end
135
+ end
@@ -46,15 +46,19 @@ class ExampleAppPhotoRevertTest < ExampleAppIntegrationTestCase
46
46
  original_size = File.size(original_path)
47
47
 
48
48
  seed_dir = Rails.root.join("db", "seed_images")
49
- jpgs = Dir.glob(seed_dir.join("*.{jpg,jpeg}"), File::FNM_CASEFOLD).sort
50
- replacement = jpgs.find { |abs| File.basename(abs) != photo.name } || jpgs.last
51
- assert replacement, "need at least one seed jpg different from the photo's current mount"
49
+ seeds = Dir.glob(seed_dir.join("*.{jpg,jpeg,png,gif}"), File::FNM_CASEFOLD).sort
50
+ replacement = seeds.find do |abs|
51
+ File.basename(abs) != photo.name && File.size(abs) != original_size
52
+ end || seeds.find { |abs| File.basename(abs) != photo.name }
53
+ assert replacement,
54
+ "need at least one seed image different from the photo's current mount"
52
55
  refute_equal File.size(replacement), original_size,
53
56
  "test needs a replacement file with a different byte length so the assertion is meaningful"
54
57
 
55
58
  frame_id = "apartment_#{@apartment.id}_photo_#{photo.id}_image"
56
59
  turbo_headers = { "Turbo-Frame" => frame_id, "Accept" => "text/html" }
57
- uploaded = Rack::Test::UploadedFile.new(replacement, "image/jpeg")
60
+ mime = (replacement.to_s.downcase.end_with?(".png") ? "image/png" : "image/jpeg")
61
+ uploaded = Rack::Test::UploadedFile.new(replacement, mime)
58
62
  put photo_path(
59
63
  photo,
60
64
  attribute: "image",
@@ -9,9 +9,9 @@ class ExampleAppPhotosTest < ExampleAppIntegrationTestCase
9
9
 
10
10
  test "photos are not served as standalone html resource" do
11
11
  assert Photo.not_accessible_through_html?
12
- assert_raises(ActionController::UnknownFormat) do
13
- get photos_path
14
- end
12
+ get photos_path
13
+ assert_not response.successful?,
14
+ "expected no standalone HTML index for not_accessible_through_html model (got #{response.status})"
15
15
  end
16
16
 
17
17
  test "can create a photo for an apartment" do
@@ -28,6 +28,11 @@
28
28
  <%= link_to "Apartment names (first 10)", apartment_name_list_path %>
29
29
  </li>
30
30
  <% end %>
31
+ <% if defined?(Owner) && (can? :read, Owner) %>
32
+ <li>
33
+ <%= link_to Owner.model_name.human.pluralize, owners_path %>
34
+ </li>
35
+ <% end %>
31
36
  </ul>
32
37
  </li>
33
38
  <% end %>
@@ -0,0 +1,30 @@
1
+ <%#
2
+ Owner sub-tabs (Turbo). Each tab link targets the surrounding row
3
+ turbo-frame (id == @update_span) so clicking a tab re-fetches
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.
8
+ -%>
9
+ <div class="row owner_tabs_row">
10
+ <div class='medium-1 large-1 column'>&nbsp;</div>
11
+ <div class='small-11 column'>
12
+ <%# Foundation 6 tabs markup: `<ul class="tabs">` floats `<li class="tabs-title">`
13
+ children horizontally and `[aria-selected="true"]` highlights the active
14
+ anchor (see foundation-rails _tabs.scss). The TurboTabsBuilder emits
15
+ `aria-selected` and merges `is-active` onto the active `<li>`. -%>
16
+ <%= tabs_tag builder: InlineForms::TurboTabsBuilder,
17
+ active_class: "is-active",
18
+ open_tabs: { class: "tabs owner_tabs",
19
+ id: "owner_#{@object.id}_tabs",
20
+ "data-tabs": "" } do |tab| %>
21
+ <% (@inline_forms_owner_tabs || OwnersController::OWNER_TABS).each do |t| %>
22
+ <%= tab.send(t,
23
+ t("owner_tabs.#{t}", default: t.titleize),
24
+ owner_path(@object, tab: t, update: @update_span),
25
+ class: "tabs-title",
26
+ link_options: { data: { turbo_frame: @update_span } }) %>
27
+ <% end %>
28
+ <% end %>
29
+ </div>
30
+ </div>
@@ -0,0 +1,9 @@
1
+ <%#
2
+ Per-owner tabbed show panel (Turbo). Replaces the stock single-pane
3
+ _show render with: tabs above, attribute subset below, both inside
4
+ the same row turbo-frame so tab switches do a single partial swap.
5
+ -%>
6
+ <turbo-frame id="<%= @update_span %>">
7
+ <%= render partial: "owners/owner_tabs" %>
8
+ <%= render partial: "inline_forms/show" %>
9
+ </turbo-frame>
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: 7.11.0
4
+ version: 7.13.11
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ace Suares
@@ -11,6 +11,20 @@ bindir: bin
11
11
  cert_chain: []
12
12
  date: 1980-01-02 00:00:00.000000000 Z
13
13
  dependencies:
14
+ - !ruby/object:Gem::Dependency
15
+ name: inline_forms
16
+ requirement: !ruby/object:Gem::Requirement
17
+ requirements:
18
+ - - "~>"
19
+ - !ruby/object:Gem::Version
20
+ version: '7'
21
+ type: :runtime
22
+ prerelease: false
23
+ version_requirements: !ruby/object:Gem::Requirement
24
+ requirements:
25
+ - - "~>"
26
+ - !ruby/object:Gem::Version
27
+ version: '7'
14
28
  - !ruby/object:Gem::Dependency
15
29
  name: rvm
16
30
  requirement: !ruby/object:Gem::Requirement
@@ -84,6 +98,7 @@ files:
84
98
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_top_level_pagination_test.rb
85
99
  - lib/installer_templates/example_app_tests/test/integration/example_app_apartment_versions_turbo_test.rb
86
100
  - lib/installer_templates/example_app_tests/test/integration/example_app_guest_access_test.rb
101
+ - lib/installer_templates/example_app_tests/test/integration/example_app_owner_tabs_test.rb
87
102
  - lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb
88
103
  - lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb
89
104
  - lib/installer_templates/example_app_tests/test/integration/example_app_routing_test.rb
@@ -95,6 +110,8 @@ files:
95
110
  - lib/installer_templates/example_app_tests/test/models/example_app_plain_text_rich_text_edge_cases_test.rb
96
111
  - lib/installer_templates/example_app_views/apartments/name_list.html.erb
97
112
  - lib/installer_templates/example_app_views/inline_forms/_header.html.erb
113
+ - lib/installer_templates/example_app_views/owners/_owner_tabs.html.erb
114
+ - lib/installer_templates/example_app_views/owners/show_with_tabs.html.erb
98
115
  - lib/installer_templates/unicorn/production.rb
99
116
  homepage: http://github.com/acesuares/inline_forms
100
117
  licenses:
@@ -107,14 +124,14 @@ required_ruby_version: !ruby/object:Gem::Requirement
107
124
  requirements:
108
125
  - - ">="
109
126
  - !ruby/object:Gem::Version
110
- version: 3.2.0
127
+ version: 4.0.0
111
128
  required_rubygems_version: !ruby/object:Gem::Requirement
112
129
  requirements:
113
130
  - - ">="
114
131
  - !ruby/object:Gem::Version
115
132
  version: '0'
116
133
  requirements: []
117
- rubygems_version: 3.7.2
134
+ rubygems_version: 4.0.10
118
135
  specification_version: 4
119
136
  summary: CLI and Rails app template for generating inline_forms applications.
120
137
  test_files: []