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 +4 -4
- data/inline_forms_installer.gemspec +2 -1
- data/lib/inline_forms_installer/creator.rb +8 -7
- data/lib/inline_forms_installer/installer_core.rb +243 -75
- data/lib/inline_forms_installer/version.rb +4 -1
- data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_photos_pagination_test.rb +22 -12
- data/lib/installer_templates/example_app_tests/test/integration/example_app_apartment_versions_turbo_test.rb +158 -0
- data/lib/installer_templates/example_app_tests/test/integration/example_app_owner_tabs_test.rb +135 -0
- data/lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb +8 -4
- data/lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb +3 -3
- data/lib/installer_templates/example_app_views/inline_forms/_header.html.erb +5 -0
- data/lib/installer_templates/example_app_views/owners/_owner_tabs.html.erb +30 -0
- data/lib/installer_templates/example_app_views/owners/show_with_tabs.html.erb +9 -0
- metadata +20 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 6c28f4c0dd8adc114abc964fa0d7ae579c14f07ef01b00e7ae9c49817db3b3d6
|
|
4
|
+
data.tar.gz: a361b6d41eaf9a5c1a4534a52d596b8925a67f8dcf9020ff7254727818aa628b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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 = ">=
|
|
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
|
-
|
|
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"] =
|
|
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.
|
|
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
|
|
11
|
-
# `
|
|
12
|
-
#
|
|
13
|
-
#
|
|
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.
|
|
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
|
-
|
|
41
|
-
#
|
|
42
|
-
#
|
|
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", "~>
|
|
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.
|
|
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',
|
|
72
|
+
gem 'tabs_on_rails', '~> 3.0'
|
|
76
73
|
gem 'unicorn'
|
|
77
|
-
gem 'validation_hints', '~>
|
|
78
|
-
gem 'will_paginate'
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
|
|
811
|
-
|
|
812
|
-
|
|
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
|
|
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
|
-
|
|
313
|
-
assert_operator
|
|
314
|
-
"need at least two seed
|
|
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 =
|
|
317
|
-
|
|
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
|
-
|
|
350
|
-
replacement =
|
|
351
|
-
|
|
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
|
-
|
|
420
|
-
|
|
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
|
-
|
|
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
|
data/lib/installer_templates/example_app_tests/test/integration/example_app_owner_tabs_test.rb
ADDED
|
@@ -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
|
data/lib/installer_templates/example_app_tests/test/integration/example_app_photo_revert_test.rb
CHANGED
|
@@ -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
|
-
|
|
50
|
-
replacement =
|
|
51
|
-
|
|
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
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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'> </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
|
|
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:
|
|
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:
|
|
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: []
|