inline_forms 6.2.14 → 7.0.4
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/CHANGELOG.md +174 -0
- data/README.rdoc +13 -1
- data/Rakefile +12 -1
- data/app/helpers/form_elements/audio_field.rb +1 -1
- data/app/helpers/form_elements/check_box.rb +1 -1
- data/app/helpers/form_elements/check_list.rb +2 -2
- data/app/helpers/form_elements/chicas_dropdown_with_family_members.rb +1 -1
- data/app/helpers/form_elements/ckeditor.rb +2 -1
- data/app/helpers/form_elements/date.rb +1 -1
- data/app/helpers/form_elements/decimal_field.rb +1 -1
- data/app/helpers/form_elements/devise_password_field.rb +1 -1
- data/app/helpers/form_elements/dropdown.rb +1 -1
- data/app/helpers/form_elements/dropdown_with_integers.rb +1 -1
- data/app/helpers/form_elements/dropdown_with_other.rb +1 -1
- data/app/helpers/form_elements/dropdown_with_values.rb +1 -1
- data/app/helpers/form_elements/dropdown_with_values_with_stars.rb +1 -1
- data/app/helpers/form_elements/file_field.rb +1 -1
- data/app/helpers/form_elements/geo_code_curacao.rb +1 -1
- data/app/helpers/form_elements/image_field.rb +1 -1
- data/app/helpers/form_elements/integer_field.rb +1 -1
- data/app/helpers/form_elements/kansen_slider.rb +1 -1
- data/app/helpers/form_elements/money_field.rb +1 -1
- data/app/helpers/form_elements/month_select.rb +1 -1
- data/app/helpers/form_elements/month_year_picker.rb +1 -1
- data/app/helpers/form_elements/move.rb +1 -1
- data/app/helpers/form_elements/multi_image_field.rb +1 -1
- data/app/helpers/form_elements/plain_text_area.rb +1 -1
- data/app/helpers/form_elements/question_list.rb +2 -2
- data/app/helpers/form_elements/radio_button.rb +1 -1
- data/app/helpers/form_elements/rich_text.rb +36 -0
- data/app/helpers/form_elements/scale_with_integers.rb +1 -1
- data/app/helpers/form_elements/scale_with_values.rb +1 -1
- data/app/helpers/form_elements/simple_file_field.rb +1 -1
- data/app/helpers/form_elements/slider_with_values.rb +1 -1
- data/app/helpers/form_elements/text_area.rb +4 -3
- data/app/helpers/form_elements/text_area_without_ckeditor.rb +1 -1
- data/app/helpers/form_elements/text_field.rb +1 -1
- data/app/helpers/form_elements/time.rb +1 -1
- data/app/helpers/inline_forms_helper.rb +56 -29
- data/app/views/inline_forms/_close.html.erb +2 -2
- data/app/views/inline_forms/_edit.html.erb +5 -4
- data/app/views/inline_forms/_new.html.erb +2 -2
- data/app/views/inline_forms/_versions_list.html.erb +30 -14
- data/app/views/layouts/application.html.erb +3 -1
- data/app/views/layouts/inline_forms.html.erb +3 -1
- data/bin/inline_forms +1 -1
- data/bin/inline_forms_installer_core.rb +99 -27
- data/docs/prompt/.gitignore +5 -0
- data/docs/prompt/test-the-example-app.md +32 -0
- data/inline_forms.gemspec +3 -1
- data/lib/generators/inline_forms_generator.rb +30 -0
- data/lib/generators/templates/migration.erb +1 -1
- data/lib/generators/templates/model.erb +1 -0
- data/lib/inline_forms/form_element_from_callee.rb +10 -0
- data/lib/inline_forms/version.rb +1 -1
- data/lib/inline_forms.rb +1 -13
- data/lib/installer_templates/example_app_tests/test/example_app/example_integration_test_case.rb +36 -0
- data/lib/installer_templates/example_app_tests/test/integration/example_app_guest_access_test.rb +21 -0
- data/lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb +22 -0
- data/lib/installer_templates/example_app_tests/test/integration/example_app_routing_test.rb +15 -0
- data/lib/installer_templates/example_app_tests/test/models/example_app_apartment_photo_test.rb +26 -0
- data/lib/installer_templates/example_app_tests/test/models/example_app_paper_trail_changeset_test.rb +78 -0
- data/test/form_element_from_callee_test.rb +73 -0
- data/test/inline_edit_polymorphic_path_test.rb +81 -0
- data/test/inline_forms_generator_test.rb +155 -0
- data/test/test_helper.rb +6 -0
- metadata +42 -11
- data/app/helpers/form_elements/absence_list.rb +0 -45
|
@@ -19,6 +19,18 @@ module InlineForms
|
|
|
19
19
|
#
|
|
20
20
|
class InlineFormsGenerator < Rails::Generators::NamedBase
|
|
21
21
|
Rails::Generators::GeneratedAttribute.class_eval do #:doc:
|
|
22
|
+
# Override Rails::Generators::GeneratedAttribute.valid_type? so that our
|
|
23
|
+
# custom field types (dropdown, check_list, image_field, rich_text, ...)
|
|
24
|
+
# pass through parsing. We do our own unknown-type detection later (with
|
|
25
|
+
# Thor::Error + --allow-unknown), so it is safe to accept everything here.
|
|
26
|
+
#
|
|
27
|
+
# Rails 6.1 used to rescue ActiveRecord::Base.connection failures, which
|
|
28
|
+
# masked the issue; Rails 7+ raises NameError when ActiveRecord is not
|
|
29
|
+
# loaded yet, breaking generator unit tests.
|
|
30
|
+
def self.valid_type?(_type)
|
|
31
|
+
true
|
|
32
|
+
end
|
|
33
|
+
|
|
22
34
|
# Deducts the column_type for migrations from the type.
|
|
23
35
|
#
|
|
24
36
|
# We first merge the Special Column Types with the Default Column Types,
|
|
@@ -71,6 +83,7 @@ module InlineForms
|
|
|
71
83
|
|
|
72
84
|
end
|
|
73
85
|
argument :attributes, :type => :array, :banner => "[name:form_element]..."
|
|
86
|
+
class_option :allow_unknown, :type => :boolean, :default => false, :desc => "Allow unknown field types (legacy behavior: comment generated lines instead of failing)."
|
|
74
87
|
|
|
75
88
|
source_root File.expand_path(File.join(File.dirname(__FILE__), 'templates'))
|
|
76
89
|
|
|
@@ -80,11 +93,24 @@ module InlineForms
|
|
|
80
93
|
@flag_create_migration = true
|
|
81
94
|
@flag_create_model = true
|
|
82
95
|
@create_id = true
|
|
96
|
+
@unknown_attributes = []
|
|
83
97
|
for attribute in attributes
|
|
84
98
|
@flag_not_accessible_through_html = false if attribute.name == '_enabled'
|
|
85
99
|
@flag_create_migration = false if attribute.name == '_no_migration'
|
|
86
100
|
@flag_create_model = false if attribute.name == '_no_model'
|
|
87
101
|
@create_id = false if attribute.name == "_id" && attribute.type == :false
|
|
102
|
+
if !attribute.name.start_with?('_') && (
|
|
103
|
+
(attribute.attribute? && attribute.attribute_type == :unknown) ||
|
|
104
|
+
(attribute.migration? && attribute.column_type == :unknown)
|
|
105
|
+
)
|
|
106
|
+
@unknown_attributes << "#{attribute.name}:#{attribute.type}"
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
allow_unknown = options[:allow_unknown].to_s == 'true'
|
|
110
|
+
if !allow_unknown && !@unknown_attributes.empty?
|
|
111
|
+
raise Thor::Error,
|
|
112
|
+
"Unknown field type(s): #{@unknown_attributes.uniq.join(', ')}. " +
|
|
113
|
+
"Add a valid form element type or pass --allow-unknown to keep legacy commented output."
|
|
88
114
|
end
|
|
89
115
|
@flag_create_controller = @flag_create_model
|
|
90
116
|
@flag_create_resource_route = @flag_create_model
|
|
@@ -96,6 +122,7 @@ module InlineForms
|
|
|
96
122
|
@has_many = "\n"
|
|
97
123
|
@has_one = "\n"
|
|
98
124
|
@habtm = "\n"
|
|
125
|
+
@has_rich_text = "\n"
|
|
99
126
|
@has_attached_files = "\n"
|
|
100
127
|
@presentation = "\n"
|
|
101
128
|
@order = "\n"
|
|
@@ -129,6 +156,9 @@ module InlineForms
|
|
|
129
156
|
attribute.type == :check_list
|
|
130
157
|
@habtm << ' has_and_belongs_to_many :' + attribute.name + "\n"
|
|
131
158
|
end
|
|
159
|
+
if attribute.type == :rich_text
|
|
160
|
+
@has_rich_text << ' has_rich_text :' + attribute.name + "\n"
|
|
161
|
+
end
|
|
132
162
|
if attribute.name == '_presentation'
|
|
133
163
|
@presentation << " def _presentation\n" +
|
|
134
164
|
" \"#{attribute.type.to_s}\"\n" +
|
|
@@ -8,6 +8,7 @@ class <%= name %> < ApplicationRecord
|
|
|
8
8
|
<%= @has_many if @has_many.length > 1 -%>
|
|
9
9
|
<%= @has_one if @has_one.length > 1 -%>
|
|
10
10
|
<%= @habtm if @habtm.length > 1 -%>
|
|
11
|
+
<%= @has_rich_text if @has_rich_text.length > 1 -%>
|
|
11
12
|
<%= @has_attached_files if @has_attached_files.length > 1 -%>
|
|
12
13
|
<%= @presentation if @presentation.length > 1 -%>
|
|
13
14
|
<%= @inline_forms_attribute_list -%>
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
# -*- encoding : utf-8 -*-
|
|
2
|
+
module InlineForms
|
|
3
|
+
# Maps +__callee__+ from a +*_show+ helper to the +params[:form_element]+ string
|
|
4
|
+
# (e.g. +:text_field_show+ → +"text_field"+).
|
|
5
|
+
def self.form_element_string_from_callee(from_callee)
|
|
6
|
+
s = from_callee.to_s
|
|
7
|
+
s = s.sub(/\Ablock in /, "")
|
|
8
|
+
s.delete_suffix("_show")
|
|
9
|
+
end
|
|
10
|
+
end
|
data/lib/inline_forms/version.rb
CHANGED
data/lib/inline_forms.rb
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
# -*- encoding : utf-8 -*-
|
|
2
2
|
require ('inline_forms/version.rb')
|
|
3
|
+
require_relative ('inline_forms/form_element_from_callee')
|
|
3
4
|
# InlineForms is a Rails Engine that let you setup an admin interface quick and
|
|
4
5
|
# easy. Please install it as a gem or include it in your Gemfile.
|
|
5
6
|
module InlineForms
|
|
@@ -153,16 +154,3 @@ module InlineForms
|
|
|
153
154
|
end
|
|
154
155
|
|
|
155
156
|
end
|
|
156
|
-
|
|
157
|
-
# make the current method and the calling method available
|
|
158
|
-
# http://www.ruby-forum.com/topic/75258
|
|
159
|
-
# supposedly, this is fixed in 1.9
|
|
160
|
-
module Kernel
|
|
161
|
-
private
|
|
162
|
-
def this_method
|
|
163
|
-
caller[0] =~ /`([^']*)'/ and $1
|
|
164
|
-
end
|
|
165
|
-
def calling_method
|
|
166
|
-
caller[1] =~ /`([^']*)'/ and $1
|
|
167
|
-
end
|
|
168
|
-
end
|
data/lib/installer_templates/example_app_tests/test/example_app/example_integration_test_case.rb
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# Base class for integration tests shipped only with `inline_forms create … --example`.
|
|
4
|
+
# See test/integration/example_app_*_test.rb
|
|
5
|
+
require "test_helper"
|
|
6
|
+
require "devise/test/integration_helpers"
|
|
7
|
+
|
|
8
|
+
class ExampleAppIntegrationTestCase < ActionDispatch::IntegrationTest
|
|
9
|
+
include Devise::Test::IntegrationHelpers
|
|
10
|
+
|
|
11
|
+
setup do
|
|
12
|
+
host!("www.example.com")
|
|
13
|
+
sign_in(example_app_admin_user)
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def example_app_admin_user
|
|
19
|
+
@example_app_admin_user ||= begin
|
|
20
|
+
locale = Locale.find_or_create_by!(name: "en") { |l| l.title = "English" }
|
|
21
|
+
role = Role.find_or_create_by!(name: "superadmin") { |r| r.description = "Super Admin" }
|
|
22
|
+
user = User.find_or_initialize_by(email: "admin@example.com")
|
|
23
|
+
if user.new_record?
|
|
24
|
+
user.assign_attributes(
|
|
25
|
+
name: "Admin",
|
|
26
|
+
password: "admin999",
|
|
27
|
+
password_confirmation: "admin999",
|
|
28
|
+
locale: locale
|
|
29
|
+
)
|
|
30
|
+
user.save!
|
|
31
|
+
end
|
|
32
|
+
user.roles << role unless user.roles.where(id: role.id).exists?
|
|
33
|
+
user
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
data/lib/installer_templates/example_app_tests/test/integration/example_app_guest_access_test.rb
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
require "devise/test/integration_helpers"
|
|
5
|
+
|
|
6
|
+
class ExampleAppGuestAccessTest < ActionDispatch::IntegrationTest
|
|
7
|
+
include Devise::Test::IntegrationHelpers
|
|
8
|
+
|
|
9
|
+
setup { host!("www.example.com") }
|
|
10
|
+
|
|
11
|
+
test "apartments index redirects when not signed in" do
|
|
12
|
+
get apartments_path
|
|
13
|
+
assert_response :redirect
|
|
14
|
+
assert_match %r{/auth/users/sign_in}, @response.redirect_url
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test "root redirects when not signed in" do
|
|
18
|
+
get root_path
|
|
19
|
+
assert_response :redirect
|
|
20
|
+
end
|
|
21
|
+
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../example_app/example_integration_test_case"
|
|
4
|
+
|
|
5
|
+
class ExampleAppPhotosTest < ExampleAppIntegrationTestCase
|
|
6
|
+
setup do
|
|
7
|
+
@apartment = Apartment.create!(name: "Beach", title: "Ocean view")
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
test "photos are not served as standalone html resource" do
|
|
11
|
+
assert Photo.not_accessible_through_html?
|
|
12
|
+
assert_raises(ActionController::UnknownFormat) do
|
|
13
|
+
get photos_path
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
test "can create a photo for an apartment" do
|
|
18
|
+
assert_difference("Photo.count", 1) do
|
|
19
|
+
Photo.create!(name: "Sunset", apartment: @apartment)
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../example_app/example_integration_test_case"
|
|
4
|
+
|
|
5
|
+
class ExampleAppRoutingTest < ExampleAppIntegrationTestCase
|
|
6
|
+
test "root routes to apartments index" do
|
|
7
|
+
get root_path
|
|
8
|
+
assert_response :success
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test "apartments index is reachable when signed in" do
|
|
12
|
+
get apartments_path
|
|
13
|
+
assert_response :success
|
|
14
|
+
end
|
|
15
|
+
end
|
data/lib/installer_templates/example_app_tests/test/models/example_app_apartment_photo_test.rb
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
class ExampleAppApartmentPhotoTest < ActiveSupport::TestCase
|
|
6
|
+
setup do
|
|
7
|
+
@apartment = Apartment.create!(name: "North", title: "Tower")
|
|
8
|
+
@photo = Photo.create!(name: "Lobby", apartment: @apartment)
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
test "photo belongs to apartment" do
|
|
12
|
+
assert_equal @apartment, @photo.apartment
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
test "apartment has many photos" do
|
|
16
|
+
assert_includes @apartment.photos.to_a, @photo
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
test "photo model hides resource from standalone html crud" do
|
|
20
|
+
assert Photo.not_accessible_through_html?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
test "apartment model allows standalone html crud" do
|
|
24
|
+
assert_not Apartment.not_accessible_through_html?
|
|
25
|
+
end
|
|
26
|
+
end
|
data/lib/installer_templates/example_app_tests/test/models/example_app_paper_trail_changeset_test.rb
ADDED
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "test_helper"
|
|
4
|
+
|
|
5
|
+
# Regression: PaperTrail (>= 13) deserializes `versions.object_changes` via
|
|
6
|
+
# `YAML.safe_load`, using `ActiveRecord.yaml_column_permitted_classes` as the
|
|
7
|
+
# allow-list. Rails 7's default is `[Symbol]`, so any update that touches
|
|
8
|
+
# `updated_at` (an `ActiveSupport::TimeWithZone`) raises `Psych::DisallowedClass`
|
|
9
|
+
# inside `version.changeset`, and PaperTrail rescues that into `{}`. The
|
|
10
|
+
# inline_forms versions list then renders every changeset as `empty`.
|
|
11
|
+
#
|
|
12
|
+
# The installer ships `config/initializers/paper_trail_yaml_safe_load.rb`,
|
|
13
|
+
# which extends the allow-list. This test fails if that initializer is
|
|
14
|
+
# missing or insufficient.
|
|
15
|
+
class ExampleAppPaperTrailChangesetTest < ActiveSupport::TestCase
|
|
16
|
+
test "version.changeset round-trips a normal update" do
|
|
17
|
+
PaperTrail.request.whodunnit = "test"
|
|
18
|
+
|
|
19
|
+
apartment = Apartment.create!(name: "Original", title: "Old Title")
|
|
20
|
+
apartment.update!(name: "Renamed", title: "New Title")
|
|
21
|
+
|
|
22
|
+
version = apartment.versions.last
|
|
23
|
+
assert_equal "update", version.event,
|
|
24
|
+
"expected an update version to be recorded by paper_trail"
|
|
25
|
+
|
|
26
|
+
changeset = version.changeset
|
|
27
|
+
assert_kind_of Hash, changeset
|
|
28
|
+
refute_empty changeset,
|
|
29
|
+
"version.changeset is empty; PaperTrail's YAML.safe_load probably hit a " \
|
|
30
|
+
"DisallowedClass and returned {}. Check " \
|
|
31
|
+
"config/initializers/paper_trail_yaml_safe_load.rb and the Rails " \
|
|
32
|
+
"ActiveRecord.yaml_column_permitted_classes setting."
|
|
33
|
+
|
|
34
|
+
assert_equal ["Original", "Renamed"], changeset["name"]
|
|
35
|
+
assert_equal ["Old Title", "New Title"], changeset["title"]
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
# `has_rich_text :description` lives in the `action_text_rich_texts` table,
|
|
39
|
+
# so `has_paper_trail` on Apartment cannot see body edits. The installer adds
|
|
40
|
+
# `config/initializers/rich_text_paper_trail.rb` (which declares
|
|
41
|
+
# `has_paper_trail` on `ActionText::RichText`), and `inline_forms_versions_for`
|
|
42
|
+
# merges rich-text versions into the parent's history. This test fails if
|
|
43
|
+
# either piece regresses.
|
|
44
|
+
test "rich_text edits surface in the merged versions list for the parent" do
|
|
45
|
+
PaperTrail.request.whodunnit = "test"
|
|
46
|
+
|
|
47
|
+
apartment = Apartment.create!(name: "RT", title: "RT", description: "v1 body")
|
|
48
|
+
apartment.update!(description: "v2 body")
|
|
49
|
+
|
|
50
|
+
rich_text = ActionText::RichText.find_by!(
|
|
51
|
+
record_type: Apartment.name, record_id: apartment.id, name: "description"
|
|
52
|
+
)
|
|
53
|
+
assert rich_text.respond_to?(:versions),
|
|
54
|
+
"ActionText::RichText is missing has_paper_trail; check " \
|
|
55
|
+
"config/initializers/rich_text_paper_trail.rb"
|
|
56
|
+
refute_empty rich_text.versions,
|
|
57
|
+
"no PaperTrail versions on ActionText::RichText; the on_load hook " \
|
|
58
|
+
"in config/initializers/rich_text_paper_trail.rb did not fire or " \
|
|
59
|
+
"has_paper_trail was not applied"
|
|
60
|
+
|
|
61
|
+
helper = Class.new { include InlineFormsHelper }.new
|
|
62
|
+
entries = helper.inline_forms_versions_for(apartment)
|
|
63
|
+
|
|
64
|
+
rich_text_entries = entries.select { |e| e[:kind] == :rich_text }
|
|
65
|
+
refute_empty rich_text_entries,
|
|
66
|
+
"inline_forms_versions_for did not return any rich_text entries"
|
|
67
|
+
|
|
68
|
+
body_changes = rich_text_entries
|
|
69
|
+
.map { |e| e[:version].changeset }
|
|
70
|
+
.select { |cs| cs && cs["body"].is_a?(Array) }
|
|
71
|
+
.map { |cs| cs["body"] }
|
|
72
|
+
|
|
73
|
+
refute_empty body_changes,
|
|
74
|
+
"no body changesets recorded for ActionText::RichText"
|
|
75
|
+
assert body_changes.any? { |old, new_| old.to_s.include?("v1 body") && new_.to_s.include?("v2 body") },
|
|
76
|
+
"expected an entry whose body change moves from 'v1 body' to 'v2 body'; got #{body_changes.inspect}"
|
|
77
|
+
end
|
|
78
|
+
end
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
|
|
5
|
+
# Documents the contract between *_show helpers, +__callee__+, and
|
|
6
|
+
# +params[:form_element]+ (used by InlineFormsController#update as +"#{form_element}_update"+).
|
|
7
|
+
class FormElementFromCalleeTest < Minitest::Test
|
|
8
|
+
def test_symbol_callee_typical_show_method_maps_to_controller_suffix
|
|
9
|
+
got = InlineForms.form_element_string_from_callee(:text_field_show)
|
|
10
|
+
assert_equal(
|
|
11
|
+
"text_field",
|
|
12
|
+
got,
|
|
13
|
+
":text_field_show must become the string \"text_field\" so the edit URL's " \
|
|
14
|
+
"form_element matches text_field_update / text_field_edit."
|
|
15
|
+
)
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def test_string_callee_same_as_symbol
|
|
19
|
+
got = InlineForms.form_element_string_from_callee("dropdown_with_values_show")
|
|
20
|
+
assert_equal(
|
|
21
|
+
"dropdown_with_values",
|
|
22
|
+
got,
|
|
23
|
+
"String callee from backtrace-style names must match Symbol behavior."
|
|
24
|
+
)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def test_block_in_prefix_from_nested_call_still_maps_correctly
|
|
28
|
+
got = InlineForms.form_element_string_from_callee("block in text_field_show")
|
|
29
|
+
assert_equal(
|
|
30
|
+
"text_field",
|
|
31
|
+
got,
|
|
32
|
+
"When *_show runs inside a block, Ruby may prefix the callee label with " \
|
|
33
|
+
"\"block in \"; that prefix must be stripped before removing _show."
|
|
34
|
+
)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def test_only_one_trailing_show_suffix_removed
|
|
38
|
+
got = InlineForms.form_element_string_from_callee(:foo_show)
|
|
39
|
+
assert_equal(
|
|
40
|
+
"foo",
|
|
41
|
+
got,
|
|
42
|
+
"delete_suffix('_show') must remove only the trailing _show once " \
|
|
43
|
+
"(not leave \"foo\" with a spurious strip)."
|
|
44
|
+
)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def test_multi_word_element_name_underscores_preserved
|
|
48
|
+
got = InlineForms.form_element_string_from_callee(:chicas_dropdown_with_family_members_show)
|
|
49
|
+
assert_equal(
|
|
50
|
+
"chicas_dropdown_with_family_members",
|
|
51
|
+
got,
|
|
52
|
+
"Form element names with underscores must survive unchanged except for _show."
|
|
53
|
+
)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def test_callee_without_show_suffix_is_unchanged_modulo_block_prefix
|
|
57
|
+
got = InlineForms.form_element_string_from_callee(:already_plain)
|
|
58
|
+
assert_equal(
|
|
59
|
+
"already_plain",
|
|
60
|
+
got,
|
|
61
|
+
"If someone passes a callee without _show, output is unchanged (minus block prefix)."
|
|
62
|
+
)
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def test_nil_callee_yields_empty_string
|
|
66
|
+
got = InlineForms.form_element_string_from_callee(nil)
|
|
67
|
+
assert_equal(
|
|
68
|
+
"",
|
|
69
|
+
got,
|
|
70
|
+
"nil.to_s is empty; callers must pass __callee__ from a *_show method."
|
|
71
|
+
)
|
|
72
|
+
end
|
|
73
|
+
end
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "logger"
|
|
5
|
+
require "active_model"
|
|
6
|
+
require "action_dispatch"
|
|
7
|
+
|
|
8
|
+
# Record double with the same +model_name+ routing key as +resources :articles+
|
|
9
|
+
# (a class nested under Minitest::Test would get +inline_edit_…_article+, not +article+).
|
|
10
|
+
class InlineFormsArticlesRouteDouble
|
|
11
|
+
include ActiveModel::Model
|
|
12
|
+
include ActiveModel::Conversion
|
|
13
|
+
|
|
14
|
+
attr_accessor :id
|
|
15
|
+
|
|
16
|
+
def self.model_name
|
|
17
|
+
ActiveModel::Name.new(self, nil, "Article")
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(id = 1)
|
|
21
|
+
@id = id
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def persisted?
|
|
25
|
+
true
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def to_param
|
|
29
|
+
id.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Class-only double for +polymorphic_path(Model)+ (collection +articles+).
|
|
34
|
+
class InlineFormsArticleClassRouteDouble
|
|
35
|
+
def self.model_name
|
|
36
|
+
ActiveModel::Name.new(self, nil, "Article")
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Parity between polymorphic URL helpers and named +edit_article_path+,
|
|
41
|
+
# +article_path+, and +articles_path+ for a standard +resources :articles+
|
|
42
|
+
# route (no full Rails app boot).
|
|
43
|
+
class InlineEditPolymorphicPathTest < Minitest::Test
|
|
44
|
+
def setup
|
|
45
|
+
@routes = ActionDispatch::Routing::RouteSet.new
|
|
46
|
+
@routes.draw { resources :articles }
|
|
47
|
+
routes = @routes
|
|
48
|
+
holder_class = Class.new do
|
|
49
|
+
include routes.url_helpers
|
|
50
|
+
end
|
|
51
|
+
holder_class.default_url_options = { only_path: true }
|
|
52
|
+
@holder = holder_class.new
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def test_edit_polymorphic_path_matches_named_edit_helper
|
|
56
|
+
article = InlineFormsArticlesRouteDouble.new(42)
|
|
57
|
+
options = {
|
|
58
|
+
attribute: "title",
|
|
59
|
+
form_element: "text_field",
|
|
60
|
+
update: "field_article_42_title"
|
|
61
|
+
}
|
|
62
|
+
poly = @holder.edit_polymorphic_path(article, **options)
|
|
63
|
+
named = @holder.edit_article_path(article, **options)
|
|
64
|
+
assert_equal named, poly
|
|
65
|
+
end
|
|
66
|
+
|
|
67
|
+
def test_member_polymorphic_path_matches_article_path
|
|
68
|
+
article = InlineFormsArticlesRouteDouble.new(7)
|
|
69
|
+
options = { update: "span", close: true }
|
|
70
|
+
poly = @holder.polymorphic_path(article, **options)
|
|
71
|
+
named = @holder.article_path(article, **options)
|
|
72
|
+
assert_equal named, poly
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def test_collection_polymorphic_path_matches_articles_path
|
|
76
|
+
options = { update: "list", parent_class: "Parent", parent_id: "3" }
|
|
77
|
+
poly = @holder.polymorphic_path(InlineFormsArticleClassRouteDouble, **options)
|
|
78
|
+
named = @holder.articles_path(**options)
|
|
79
|
+
assert_equal named, poly
|
|
80
|
+
end
|
|
81
|
+
end
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "test_helper"
|
|
4
|
+
require "tmpdir"
|
|
5
|
+
require "fileutils"
|
|
6
|
+
require "logger"
|
|
7
|
+
require "rails"
|
|
8
|
+
require "rails/generators"
|
|
9
|
+
require "inline_forms"
|
|
10
|
+
require_relative "../lib/generators/inline_forms_generator"
|
|
11
|
+
|
|
12
|
+
class InlineFormsGeneratorTest < Minitest::Test
|
|
13
|
+
def setup
|
|
14
|
+
@destination_root = Dir.mktmpdir("inline_forms_generator_test")
|
|
15
|
+
build_destination_skeleton!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def teardown
|
|
19
|
+
FileUtils.remove_entry(@destination_root) if @destination_root && Dir.exist?(@destination_root)
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test_generates_model_controller_route_migration_and_tab_injection
|
|
23
|
+
run_generator(
|
|
24
|
+
"Thing",
|
|
25
|
+
"name:string",
|
|
26
|
+
"category:dropdown",
|
|
27
|
+
"photos:has_many",
|
|
28
|
+
"_enabled:yes",
|
|
29
|
+
"_presentation:\\#{name}"
|
|
30
|
+
)
|
|
31
|
+
|
|
32
|
+
model = read("app/models/thing.rb")
|
|
33
|
+
controller = read("app/controllers/things_controller.rb")
|
|
34
|
+
routes = read("config/routes.rb")
|
|
35
|
+
application_controller = read("app/controllers/application_controller.rb")
|
|
36
|
+
migration = read_single_migration_for("things")
|
|
37
|
+
|
|
38
|
+
assert_includes(model, "class Thing < ApplicationRecord")
|
|
39
|
+
assert_includes(model, "belongs_to :category")
|
|
40
|
+
assert_includes(model, "has_many :photos")
|
|
41
|
+
assert_includes(model, "[ :name , \"name\", :text_field ]")
|
|
42
|
+
assert_includes(model, "[ :category , \"category\", :dropdown ]")
|
|
43
|
+
|
|
44
|
+
assert_includes(controller, "class ThingsController < InlineFormsController")
|
|
45
|
+
assert_includes(controller, "set_tab :thing")
|
|
46
|
+
|
|
47
|
+
assert_includes(routes, "resources :things do")
|
|
48
|
+
assert_includes(routes, "post 'revert', :on => :member")
|
|
49
|
+
assert_includes(routes, "get 'list_versions', :on => :member")
|
|
50
|
+
|
|
51
|
+
assert_includes(application_controller, "MODEL_TABS = %w(things ")
|
|
52
|
+
|
|
53
|
+
assert_includes(migration, "class InlineFormsCreateThings < ActiveRecord::Migration[7.0]")
|
|
54
|
+
assert_includes(migration, "create_table :things do |t|")
|
|
55
|
+
assert_includes(migration, "t.string :name")
|
|
56
|
+
assert_includes(migration, "t.belongs_to :category")
|
|
57
|
+
refute_includes(migration, "photos")
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def test_skips_model_controller_and_routes_when_no_model_flag_is_present
|
|
61
|
+
run_generator("AuditLog", "message:string", "_no_model:yes")
|
|
62
|
+
|
|
63
|
+
refute_exist("app/models/audit_log.rb")
|
|
64
|
+
refute_exist("app/controllers/audit_logs_controller.rb")
|
|
65
|
+
|
|
66
|
+
routes = read("config/routes.rb")
|
|
67
|
+
refute_includes(routes, "resources :audit_logs")
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def test_fails_fast_for_unknown_types_by_default
|
|
71
|
+
stdout, stderr = capture_io do
|
|
72
|
+
run_generator("Mystery", "payload:not_a_real_type", "--no-allow-unknown")
|
|
73
|
+
end
|
|
74
|
+
output = "#{stdout}#{stderr}"
|
|
75
|
+
|
|
76
|
+
assert_includes(output, "Unknown field type(s): payload:not_a_real_type")
|
|
77
|
+
assert_includes(output, "--allow-unknown")
|
|
78
|
+
refute_exist("app/models/mystery.rb")
|
|
79
|
+
assert_equal([], Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_create_mysteries.rb")))
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def test_allow_unknown_keeps_legacy_commented_output
|
|
83
|
+
run_generator("Mystery", "payload:not_a_real_type", "--allow-unknown")
|
|
84
|
+
|
|
85
|
+
model = read("app/models/mystery.rb")
|
|
86
|
+
migration = read_single_migration_for("mysteries")
|
|
87
|
+
|
|
88
|
+
assert_includes(model, "# [ :payload , \"payload\", :unknown ]")
|
|
89
|
+
assert_includes(migration, "# t.unknown :payload")
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def test_rich_text_adds_has_rich_text_without_migration_column
|
|
93
|
+
run_generator("Article", "title:string", "content:rich_text")
|
|
94
|
+
|
|
95
|
+
model = read("app/models/article.rb")
|
|
96
|
+
migration = read_single_migration_for("articles")
|
|
97
|
+
|
|
98
|
+
assert_includes(model, "has_rich_text :content")
|
|
99
|
+
refute_includes(migration, "t.no_migration :content")
|
|
100
|
+
refute_includes(migration, "t.text :content")
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
private
|
|
104
|
+
|
|
105
|
+
def build_destination_skeleton!
|
|
106
|
+
mkdir_p("config")
|
|
107
|
+
mkdir_p("app/controllers")
|
|
108
|
+
mkdir_p("app/models")
|
|
109
|
+
mkdir_p("db/migrate")
|
|
110
|
+
mkdir_p("test/unit")
|
|
111
|
+
|
|
112
|
+
write(
|
|
113
|
+
"config/routes.rb",
|
|
114
|
+
<<~RUBY
|
|
115
|
+
Rails.application.routes.draw do
|
|
116
|
+
end
|
|
117
|
+
RUBY
|
|
118
|
+
)
|
|
119
|
+
|
|
120
|
+
write(
|
|
121
|
+
"app/controllers/application_controller.rb",
|
|
122
|
+
<<~RUBY
|
|
123
|
+
class ApplicationController < ActionController::Base
|
|
124
|
+
ActionView::CompiledTemplates::MODEL_TABS = %w()
|
|
125
|
+
end
|
|
126
|
+
RUBY
|
|
127
|
+
)
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def run_generator(*args)
|
|
131
|
+
InlineForms::InlineFormsGenerator.start(args, destination_root: @destination_root)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def read(relative_path)
|
|
135
|
+
File.read(File.join(@destination_root, relative_path))
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def read_single_migration_for(table_name)
|
|
139
|
+
migration_files = Dir.glob(File.join(@destination_root, "db/migrate/*_inline_forms_create_#{table_name}.rb"))
|
|
140
|
+
assert_equal(1, migration_files.size, "Expected one migration for #{table_name}, got #{migration_files.inspect}")
|
|
141
|
+
File.read(migration_files.first)
|
|
142
|
+
end
|
|
143
|
+
|
|
144
|
+
def refute_exist(relative_path)
|
|
145
|
+
refute(File.exist?(File.join(@destination_root, relative_path)), "Expected #{relative_path} to not exist")
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
def mkdir_p(relative_path)
|
|
149
|
+
FileUtils.mkdir_p(File.join(@destination_root, relative_path))
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
def write(relative_path, content)
|
|
153
|
+
File.write(File.join(@destination_root, relative_path), content)
|
|
154
|
+
end
|
|
155
|
+
end
|