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.
Files changed (69) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +174 -0
  3. data/README.rdoc +13 -1
  4. data/Rakefile +12 -1
  5. data/app/helpers/form_elements/audio_field.rb +1 -1
  6. data/app/helpers/form_elements/check_box.rb +1 -1
  7. data/app/helpers/form_elements/check_list.rb +2 -2
  8. data/app/helpers/form_elements/chicas_dropdown_with_family_members.rb +1 -1
  9. data/app/helpers/form_elements/ckeditor.rb +2 -1
  10. data/app/helpers/form_elements/date.rb +1 -1
  11. data/app/helpers/form_elements/decimal_field.rb +1 -1
  12. data/app/helpers/form_elements/devise_password_field.rb +1 -1
  13. data/app/helpers/form_elements/dropdown.rb +1 -1
  14. data/app/helpers/form_elements/dropdown_with_integers.rb +1 -1
  15. data/app/helpers/form_elements/dropdown_with_other.rb +1 -1
  16. data/app/helpers/form_elements/dropdown_with_values.rb +1 -1
  17. data/app/helpers/form_elements/dropdown_with_values_with_stars.rb +1 -1
  18. data/app/helpers/form_elements/file_field.rb +1 -1
  19. data/app/helpers/form_elements/geo_code_curacao.rb +1 -1
  20. data/app/helpers/form_elements/image_field.rb +1 -1
  21. data/app/helpers/form_elements/integer_field.rb +1 -1
  22. data/app/helpers/form_elements/kansen_slider.rb +1 -1
  23. data/app/helpers/form_elements/money_field.rb +1 -1
  24. data/app/helpers/form_elements/month_select.rb +1 -1
  25. data/app/helpers/form_elements/month_year_picker.rb +1 -1
  26. data/app/helpers/form_elements/move.rb +1 -1
  27. data/app/helpers/form_elements/multi_image_field.rb +1 -1
  28. data/app/helpers/form_elements/plain_text_area.rb +1 -1
  29. data/app/helpers/form_elements/question_list.rb +2 -2
  30. data/app/helpers/form_elements/radio_button.rb +1 -1
  31. data/app/helpers/form_elements/rich_text.rb +36 -0
  32. data/app/helpers/form_elements/scale_with_integers.rb +1 -1
  33. data/app/helpers/form_elements/scale_with_values.rb +1 -1
  34. data/app/helpers/form_elements/simple_file_field.rb +1 -1
  35. data/app/helpers/form_elements/slider_with_values.rb +1 -1
  36. data/app/helpers/form_elements/text_area.rb +4 -3
  37. data/app/helpers/form_elements/text_area_without_ckeditor.rb +1 -1
  38. data/app/helpers/form_elements/text_field.rb +1 -1
  39. data/app/helpers/form_elements/time.rb +1 -1
  40. data/app/helpers/inline_forms_helper.rb +56 -29
  41. data/app/views/inline_forms/_close.html.erb +2 -2
  42. data/app/views/inline_forms/_edit.html.erb +5 -4
  43. data/app/views/inline_forms/_new.html.erb +2 -2
  44. data/app/views/inline_forms/_versions_list.html.erb +30 -14
  45. data/app/views/layouts/application.html.erb +3 -1
  46. data/app/views/layouts/inline_forms.html.erb +3 -1
  47. data/bin/inline_forms +1 -1
  48. data/bin/inline_forms_installer_core.rb +99 -27
  49. data/docs/prompt/.gitignore +5 -0
  50. data/docs/prompt/test-the-example-app.md +32 -0
  51. data/inline_forms.gemspec +3 -1
  52. data/lib/generators/inline_forms_generator.rb +30 -0
  53. data/lib/generators/templates/migration.erb +1 -1
  54. data/lib/generators/templates/model.erb +1 -0
  55. data/lib/inline_forms/form_element_from_callee.rb +10 -0
  56. data/lib/inline_forms/version.rb +1 -1
  57. data/lib/inline_forms.rb +1 -13
  58. data/lib/installer_templates/example_app_tests/test/example_app/example_integration_test_case.rb +36 -0
  59. data/lib/installer_templates/example_app_tests/test/integration/example_app_guest_access_test.rb +21 -0
  60. data/lib/installer_templates/example_app_tests/test/integration/example_app_photos_test.rb +22 -0
  61. data/lib/installer_templates/example_app_tests/test/integration/example_app_routing_test.rb +15 -0
  62. data/lib/installer_templates/example_app_tests/test/models/example_app_apartment_photo_test.rb +26 -0
  63. data/lib/installer_templates/example_app_tests/test/models/example_app_paper_trail_changeset_test.rb +78 -0
  64. data/test/form_element_from_callee_test.rb +73 -0
  65. data/test/inline_edit_polymorphic_path_test.rb +81 -0
  66. data/test/inline_forms_generator_test.rb +155 -0
  67. data/test/test_helper.rb +6 -0
  68. metadata +42 -11
  69. 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" +
@@ -1,4 +1,4 @@
1
- class InlineFormsCreate<%= table_name.camelize %> < ActiveRecord::Migration[6.1]
1
+ class InlineFormsCreate<%= table_name.camelize %> < ActiveRecord::Migration[7.0]
2
2
 
3
3
  def self.up
4
4
  create_table :<%= table_name + @primary_key_option %> do |t|
@@ -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
@@ -1,4 +1,4 @@
1
1
  # -*- encoding : utf-8 -*-
2
2
  module InlineForms
3
- VERSION = "6.2.14"
3
+ VERSION = "7.0.4"
4
4
  end
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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -0,0 +1,6 @@
1
+ # frozen_string_literal: true
2
+
3
+ $LOAD_PATH.unshift File.expand_path("../lib", __dir__)
4
+
5
+ require "minitest/autorun"
6
+ require "inline_forms/form_element_from_callee"