active_element 0.0.10 → 0.0.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.
Files changed (214) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -2
  3. data/.strong_versions.yml +1 -0
  4. data/Gemfile +4 -0
  5. data/Gemfile.lock +108 -75
  6. data/Makefile +10 -0
  7. data/active_element.gemspec +1 -1
  8. data/app/assets/javascripts/active_element/application.js +1 -0
  9. data/app/assets/javascripts/active_element/form.js +16 -32
  10. data/app/assets/javascripts/active_element/json_field.js +391 -135
  11. data/app/assets/javascripts/active_element/setup.js +13 -8
  12. data/app/assets/javascripts/active_element/text_search_field.js +27 -28
  13. data/app/assets/javascripts/active_element/theme.js +1 -1
  14. data/app/assets/javascripts/active_element/timezones.js +6 -0
  15. data/app/assets/stylesheets/active_element/_dark.scss +86 -0
  16. data/app/assets/stylesheets/active_element/_variables.scss +2 -1
  17. data/app/assets/stylesheets/active_element/application.scss +166 -33
  18. data/app/controllers/active_element/application_controller.rb +5 -0
  19. data/app/controllers/concerns/active_element/default_controller_actions.rb +38 -0
  20. data/app/views/active_element/_user.html.erb +20 -0
  21. data/app/views/active_element/components/fields/_json.html.erb +24 -0
  22. data/app/views/active_element/components/form/_check_box.html.erb +1 -0
  23. data/app/views/active_element/components/form/_check_boxes.html.erb +1 -1
  24. data/app/views/active_element/components/form/_datetime_range_field.html.erb +14 -0
  25. data/app/views/active_element/components/form/_field.html.erb +10 -7
  26. data/app/views/active_element/components/form/_generic_field.html.erb +1 -0
  27. data/app/views/active_element/components/form/_json.html.erb +10 -2
  28. data/app/views/active_element/components/form/_label.html.erb +12 -1
  29. data/app/views/active_element/components/form/_select.html.erb +4 -1
  30. data/app/views/active_element/components/form/_summary.html.erb +11 -1
  31. data/app/views/active_element/components/form/_templates.html.erb +37 -22
  32. data/app/views/active_element/components/form/_text_area.html.erb +2 -1
  33. data/app/views/active_element/components/form/_text_search.html.erb +7 -3
  34. data/app/views/active_element/components/form.html.erb +20 -17
  35. data/app/views/active_element/components/json.html.erb +1 -0
  36. data/app/views/active_element/components/navbar.html.erb +26 -0
  37. data/app/views/active_element/components/table/_collection_row.html.erb +2 -1
  38. data/app/views/active_element/components/table/_field.html.erb +8 -0
  39. data/app/views/active_element/components/table/collection.html.erb +1 -1
  40. data/app/views/active_element/components/table/item.html.erb +5 -4
  41. data/app/views/active_element/default_views/edit.html.erb +5 -0
  42. data/app/views/active_element/default_views/index.html.erb +15 -0
  43. data/app/views/active_element/default_views/new.html.erb +4 -0
  44. data/app/views/active_element/default_views/show.html.erb +7 -0
  45. data/app/views/active_element/navbar/_menu.html.erb +1 -30
  46. data/app/views/active_element/theme/_select.html.erb +1 -1
  47. data/app/views/layouts/active_element.html.erb +16 -1
  48. data/config/brakeman.ignore +48 -0
  49. data/example_app/.gitattributes +7 -0
  50. data/example_app/.gitignore +35 -0
  51. data/example_app/.ruby-version +1 -0
  52. data/example_app/Gemfile +34 -0
  53. data/example_app/Gemfile.lock +296 -0
  54. data/example_app/README.md +24 -0
  55. data/example_app/Rakefile +6 -0
  56. data/example_app/app/assets/config/manifest.js +4 -0
  57. data/example_app/app/assets/images/.keep +0 -0
  58. data/example_app/app/assets/stylesheets/application.css +15 -0
  59. data/example_app/app/channels/application_cable/channel.rb +4 -0
  60. data/example_app/app/channels/application_cable/connection.rb +4 -0
  61. data/example_app/app/controllers/application_controller.rb +12 -0
  62. data/example_app/app/controllers/concerns/.keep +0 -0
  63. data/example_app/app/controllers/pets_controller.rb +6 -0
  64. data/example_app/app/controllers/users_controller.rb +6 -0
  65. data/example_app/app/helpers/application_helper.rb +2 -0
  66. data/example_app/app/javascript/application.js +3 -0
  67. data/example_app/app/javascript/controllers/application.js +9 -0
  68. data/example_app/app/javascript/controllers/hello_controller.js +7 -0
  69. data/example_app/app/javascript/controllers/index.js +11 -0
  70. data/example_app/app/jobs/application_job.rb +7 -0
  71. data/example_app/app/mailers/application_mailer.rb +4 -0
  72. data/example_app/app/models/application_record.rb +3 -0
  73. data/example_app/app/models/concerns/.keep +0 -0
  74. data/example_app/app/models/pet.rb +3 -0
  75. data/example_app/app/models/user.rb +8 -0
  76. data/example_app/app/views/layouts/application.html.erb +16 -0
  77. data/example_app/app/views/layouts/mailer.html.erb +13 -0
  78. data/example_app/app/views/layouts/mailer.text.erb +1 -0
  79. data/example_app/app/views/pets/index.html.erb +3 -0
  80. data/example_app/app/views/users/show.html.erb +3 -0
  81. data/example_app/bin/bundle +109 -0
  82. data/example_app/bin/importmap +4 -0
  83. data/example_app/bin/rails +4 -0
  84. data/example_app/bin/rake +4 -0
  85. data/example_app/bin/setup +33 -0
  86. data/example_app/config/application.rb +22 -0
  87. data/example_app/config/boot.rb +4 -0
  88. data/example_app/config/cable.yml +10 -0
  89. data/example_app/config/credentials.yml.enc +1 -0
  90. data/example_app/config/database.yml +25 -0
  91. data/example_app/config/environment.rb +5 -0
  92. data/example_app/config/environments/development.rb +70 -0
  93. data/example_app/config/environments/production.rb +93 -0
  94. data/example_app/config/environments/test.rb +60 -0
  95. data/example_app/config/importmap.rb +7 -0
  96. data/example_app/config/initializers/assets.rb +12 -0
  97. data/example_app/config/initializers/content_security_policy.rb +25 -0
  98. data/example_app/config/initializers/devise.rb +16 -0
  99. data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
  100. data/example_app/config/initializers/inflections.rb +16 -0
  101. data/example_app/config/initializers/permissions_policy.rb +11 -0
  102. data/example_app/config/locales/devise.en.yml +65 -0
  103. data/example_app/config/locales/en.yml +33 -0
  104. data/example_app/config/puma.rb +43 -0
  105. data/example_app/config/routes.rb +8 -0
  106. data/example_app/config/storage.yml +34 -0
  107. data/example_app/config.ru +6 -0
  108. data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
  109. data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
  110. data/example_app/db/schema.rb +37 -0
  111. data/example_app/db/seeds.rb +33 -0
  112. data/example_app/lib/assets/.keep +0 -0
  113. data/example_app/lib/tasks/.keep +0 -0
  114. data/example_app/log/.keep +0 -0
  115. data/example_app/public/404.html +67 -0
  116. data/example_app/public/422.html +67 -0
  117. data/example_app/public/500.html +66 -0
  118. data/example_app/public/apple-touch-icon-precomposed.png +0 -0
  119. data/example_app/public/apple-touch-icon.png +0 -0
  120. data/example_app/public/favicon.ico +0 -0
  121. data/example_app/public/robots.txt +1 -0
  122. data/example_app/storage/.keep +0 -0
  123. data/example_app/test/application_system_test_case.rb +5 -0
  124. data/example_app/test/channels/application_cable/connection_test.rb +11 -0
  125. data/example_app/test/controllers/.keep +0 -0
  126. data/example_app/test/fixtures/files/.keep +0 -0
  127. data/example_app/test/fixtures/users.yml +11 -0
  128. data/example_app/test/helpers/.keep +0 -0
  129. data/example_app/test/integration/.keep +0 -0
  130. data/example_app/test/mailers/.keep +0 -0
  131. data/example_app/test/models/.keep +0 -0
  132. data/example_app/test/models/user_test.rb +7 -0
  133. data/example_app/test/system/.keep +0 -0
  134. data/example_app/test/test_helper.rb +13 -0
  135. data/example_app/tmp/.keep +0 -0
  136. data/example_app/tmp/pids/.keep +0 -0
  137. data/example_app/tmp/storage/.keep +0 -0
  138. data/example_app/vendor/.keep +0 -0
  139. data/example_app/vendor/javascript/.keep +0 -0
  140. data/lib/active_element/component.rb +9 -2
  141. data/lib/active_element/components/collection_table.rb +9 -2
  142. data/lib/active_element/components/email_fields.rb +14 -0
  143. data/lib/active_element/components/form.rb +48 -17
  144. data/lib/active_element/components/navbar.rb +64 -0
  145. data/lib/active_element/components/phone_fields.rb +14 -0
  146. data/lib/active_element/components/text_search/authorization.rb +9 -6
  147. data/lib/active_element/components/text_search/component.rb +4 -2
  148. data/lib/active_element/components/text_search.rb +4 -0
  149. data/lib/active_element/components/util/association_mapping.rb +74 -19
  150. data/lib/active_element/components/util/display_value_mapping.rb +13 -4
  151. data/lib/active_element/components/util/form_field_mapping.rb +127 -10
  152. data/lib/active_element/components/util/form_value_mapping.rb +3 -3
  153. data/lib/active_element/components/util/i18n.rb +1 -1
  154. data/lib/active_element/components/util/record_mapping.rb +43 -11
  155. data/lib/active_element/components/util/record_path.rb +21 -4
  156. data/lib/active_element/components/util.rb +12 -5
  157. data/lib/active_element/components.rb +3 -0
  158. data/lib/active_element/controller_action.rb +8 -2
  159. data/lib/active_element/controller_interface.rb +47 -5
  160. data/lib/active_element/default_controller.rb +93 -0
  161. data/lib/active_element/default_record_params.rb +62 -0
  162. data/lib/active_element/default_text_search.rb +110 -0
  163. data/lib/active_element/json_field_schema.rb +59 -0
  164. data/lib/active_element/pre_render_processors/json.rb +98 -0
  165. data/lib/active_element/pre_render_processors.rb +11 -0
  166. data/lib/active_element/route.rb +12 -0
  167. data/lib/active_element/routes.rb +2 -1
  168. data/lib/active_element/version.rb +1 -1
  169. data/lib/active_element.rb +14 -32
  170. data/lib/tasks/active_element.rake +12 -1
  171. data/rspec-documentation/_head.html.erb +34 -0
  172. data/rspec-documentation/pages/000-Introduction.md +18 -0
  173. data/rspec-documentation/pages/005-Setup.md +75 -0
  174. data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
  175. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
  176. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
  177. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
  178. data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
  179. data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
  180. data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
  181. data/rspec-documentation/pages/010-Components/Forms.md +44 -0
  182. data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
  183. data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
  184. data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
  185. data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
  186. data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
  187. data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
  188. data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
  189. data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
  190. data/rspec-documentation/pages/010-Components/Tables.md +29 -0
  191. data/rspec-documentation/pages/010-Components.md +15 -0
  192. data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
  193. data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
  194. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
  195. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
  196. data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
  197. data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
  198. data/rspec-documentation/pages/020-Access Control.md +31 -0
  199. data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
  200. data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
  201. data/rspec-documentation/pages/040-Decorators.md +12 -0
  202. data/rspec-documentation/pages/300-Alternatives.md +21 -0
  203. data/rspec-documentation/pages/900-License.md +11 -0
  204. data/rspec-documentation/spec_helper.rb +53 -16
  205. data/rspec-documentation/support.rb +84 -0
  206. metadata +155 -14
  207. data/rspec-documentation/pages/Components/Forms.md +0 -1
  208. data/rspec-documentation/pages/Components/Tables.md +0 -47
  209. data/rspec-documentation/pages/Components.md +0 -1
  210. data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
  211. data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
  212. data/rspec-documentation/pages/Index.md +0 -3
  213. data/rspec-documentation/pages/Util/I18n.md +0 -1
  214. /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -13,6 +13,10 @@ module ActiveElement
13
13
 
14
14
  def path
15
15
  record_path || sti_record_path
16
+ rescue NoMethodError
17
+ raise Error,
18
+ "Unable to map #{record.inspect} to a Rails route. Tried:\n" \
19
+ "#{[default_record_path, sti_record_path].compact.join("\n")}"
16
20
  end
17
21
 
18
22
  private
@@ -36,9 +40,20 @@ module ActiveElement
36
40
  def record_path
37
41
  return nil if record.nil?
38
42
 
39
- controller.helpers.public_send(default_record_path, record)
43
+ controller.helpers.public_send(default_record_path, *path_arguments)
40
44
  rescue NoMethodError
41
- controller.helpers.public_send(sti_record_path, record)
45
+ raise NoMethodError if sti_record_name.nil?
46
+
47
+ controller.helpers.public_send(sti_record_path, *path_arguments)
48
+ end
49
+
50
+ def path_arguments
51
+ case type
52
+ when :edit, :update, :show, :destroy
53
+ [record]
54
+ when :new, :index
55
+ []
56
+ end
42
57
  end
43
58
 
44
59
  def default_record_path
@@ -46,6 +61,8 @@ module ActiveElement
46
61
  end
47
62
 
48
63
  def sti_record_path
64
+ return nil if sti_record_name.nil?
65
+
49
66
  "#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
50
67
  end
51
68
 
@@ -63,9 +80,9 @@ module ActiveElement
63
80
 
64
81
  def record_path_prefix
65
82
  case type
66
- when :edit, :update
83
+ when :edit
67
84
  'edit_'
68
- when :new, :create
85
+ when :new
69
86
  'new_'
70
87
  end
71
88
  end
@@ -29,18 +29,25 @@ module ActiveElement
29
29
  end
30
30
 
31
31
  def self.default_record_name(record)
32
- record.class.name.demodulize.underscore
32
+ (record.is_a?(Class) ? record.name : record.class.name).demodulize.underscore
33
+ end
34
+
35
+ def self.relation_controller(model, controller, relation)
36
+ namespace = controller.controller_path.rpartition('/').first.presence
37
+ base = "#{model.reflect_on_association(relation).klass.name.pluralize}Controller"
38
+ return base.safe_constantize if namespace.nil?
39
+
40
+ "#{namespace.classify}::#{base}".safe_constantize || base.safe_constantize
33
41
  end
34
42
 
35
43
  def self.json_pretty_print(json)
36
- theme = Rouge::Themes::Base16.mode(:light)
37
- formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
44
+ formatter = Rouge::Formatters::HTML.new
38
45
  lexer = Rouge::Lexers::JSON.new
39
46
  content = JSON.pretty_generate(json.is_a?(String) ? JSON.parse(json) : json)
40
- formatted = formatter.format(lexer.lex(content)).gsub(' ', '  ')
47
+ formatted = formatter.format(lexer.lex(content)).gsub(' ', '&nbsp;&nbsp;').gsub("\n", '<br/>')
41
48
  # rubocop:disable Rails/OutputSafety
42
49
  # TODO: Move to a template.
43
- "<div style='font-family: monospace;'>#{formatted}</div>".html_safe
50
+ "<div class='json-highlight' style='font-family: monospace;'>#{formatted}</div>".html_safe
44
51
  # rubocop:enable Rails/OutputSafety
45
52
  end
46
53
  end
@@ -2,8 +2,11 @@
2
2
 
3
3
  require_relative 'components/translations'
4
4
  require_relative 'components/secret_fields'
5
+ require_relative 'components/email_fields'
6
+ require_relative 'components/phone_fields'
5
7
  require_relative 'components/util'
6
8
  require_relative 'components/link_helpers'
9
+ require_relative 'components/navbar'
7
10
  require_relative 'components/page_description'
8
11
  require_relative 'components/form'
9
12
  require_relative 'components/collection_table'
@@ -11,7 +11,7 @@ module ActiveElement
11
11
 
12
12
  def process_action
13
13
  Rails.logger.info("#{ActiveElement.log_tag} #{colorized_permissions_message}")
14
- return if verified_permissions?
14
+ process_pre_render and return if verified_permissions?
15
15
 
16
16
  warn "#{ActiveElement.log_tag} #{colorized_permissions_message}" if Rails.env.test?
17
17
  return controller.redirect_to redirect_path if redirect_to_default_landing_page?
@@ -30,6 +30,12 @@ module ActiveElement
30
30
  (@verified_permissions = permissions_check.permitted?)
31
31
  end
32
32
 
33
+ def process_pre_render
34
+ PreRenderProcessors::Json.new(controller: controller).process
35
+
36
+ true
37
+ end
38
+
33
39
  def redirect_path
34
40
  routes.alternative_routes.first.path
35
41
  end
@@ -72,7 +78,7 @@ module ActiveElement
72
78
  color = if permissions_check.permitted?
73
79
  :green
74
80
  else
75
- (rails_component.environment == 'test' ? :yellow : :red)
81
+ rails_component.environment == 'test' ? :yellow : :red
76
82
  end
77
83
  paintbrush { public_send(color, permissions_check.message) }
78
84
  end
@@ -5,7 +5,7 @@ module ActiveElement
5
5
  # Encapsulates core functionality such as `authenticate_with`, `permit_action`, and `component`
6
6
  # without polluting application controller namespace.
7
7
  class ControllerInterface
8
- attr_reader :missing_template_store, :current_user
8
+ attr_reader :missing_template_store, :current_user, :assigned_editable_fields
9
9
 
10
10
  @state = {}
11
11
 
@@ -25,6 +25,22 @@ module ActiveElement
25
25
  @authorize
26
26
  end
27
27
 
28
+ def listable_fields(*args)
29
+ state[:listable_fields] = args.map(&:to_sym)
30
+ end
31
+
32
+ def viewable_fields(*args)
33
+ state[:viewable_fields] = args.map(&:to_sym)
34
+ end
35
+
36
+ def editable_fields(*args)
37
+ state[:editable_fields] = args.map(&:to_sym)
38
+ end
39
+
40
+ def searchable_fields(*args)
41
+ state[:searchable_fields] = args.map(&:to_sym)
42
+ end
43
+
28
44
  def application_name
29
45
  RailsComponent.new(::Rails).application_name
30
46
  end
@@ -38,6 +54,32 @@ module ActiveElement
38
54
  state[:authorizor] = block
39
55
  end
40
56
 
57
+ def sign_out_with(method: :get, &block)
58
+ state[:sign_out_method] = method
59
+ state[:sign_out_path] = block
60
+ end
61
+
62
+ def sign_out_path
63
+ state[:sign_out_path]&.call
64
+ end
65
+
66
+ def sign_out_method
67
+ state[:sign_out_method]
68
+ end
69
+
70
+ def sign_in_with(method: :get, &block)
71
+ state[:sign_in_method] = method
72
+ state[:sign_in_path] = block
73
+ end
74
+
75
+ def sign_in_path
76
+ state[:sign_in_path]&.call
77
+ end
78
+
79
+ def sign_in_method
80
+ state[:sign_in_method]
81
+ end
82
+
41
83
  def authenticate
42
84
  authenticator&.call
43
85
  @current_user = state[:authorizor]&.call
@@ -66,6 +108,10 @@ module ActiveElement
66
108
  raise ArgumentError, 'Attempted to use ActiveElement component from a controller class method.'
67
109
  end
68
110
 
111
+ def state
112
+ self.class.state[controller_class]
113
+ end
114
+
69
115
  private
70
116
 
71
117
  attr_reader :controller_class, :controller_instance
@@ -73,9 +119,5 @@ module ActiveElement
73
119
  def initialize_state
74
120
  self.class.state[controller_class] ||= { permissions: [], authenticator: nil }
75
121
  end
76
-
77
- def state
78
- self.class.state[controller_class]
79
- end
80
122
  end
81
123
  end
@@ -0,0 +1,93 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Encapsulation of all logic performed for default controller actions when no action is defined
5
+ # by the current controller.
6
+ class DefaultController
7
+ def initialize(controller:)
8
+ @controller = controller
9
+ end
10
+
11
+ def index
12
+ controller.render 'active_element/default_views/index',
13
+ locals: {
14
+ collection: collection,
15
+ search_filters: default_text_search.search_filters
16
+ }
17
+ end
18
+
19
+ def show
20
+ controller.render 'active_element/default_views/show', locals: { record: record }
21
+ end
22
+
23
+ def new
24
+ controller.render 'active_element/default_views/new', locals: { record: model.new, namespace: namespace }
25
+ end
26
+
27
+ def create # rubocop:disable Metrics/AbcSize
28
+ new_record = model.new(default_record_params.params)
29
+ # Ensure associations are applied:
30
+ if new_record.save && new_record.reload.update(default_record_params.params)
31
+ controller.flash.notice = "#{new_record.model_name.to_s.titleize} created successfully."
32
+ controller.redirect_to record_path(new_record, :show).path
33
+ else
34
+ controller.flash.now.alert = "Failed to create #{model.name.to_s.titleize}."
35
+ controller.render 'active_element/default_views/new', locals: { record: new_record, namespace: namespace }
36
+ end
37
+ end
38
+
39
+ def edit
40
+ controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
41
+ end
42
+
43
+ def update # rubocop:disable Metrics/AbcSize
44
+ if record.update(default_record_params.params)
45
+ controller.flash.notice = "#{record.model_name.to_s.titleize} updated successfully."
46
+ controller.redirect_to record_path(record, :show).path
47
+ else
48
+ controller.flash.now.alert = "Failed to update #{model.name.to_s.titleize}."
49
+ controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
50
+ end
51
+ end
52
+
53
+ def destroy
54
+ record.destroy
55
+ controller.flash.notice = "Deleted #{record.model_name.to_s.titleize}."
56
+ controller.redirect_to record_path(model, :index).path
57
+ end
58
+
59
+ private
60
+
61
+ attr_reader :controller
62
+
63
+ def default_record_params
64
+ @default_record_params ||= ActiveElement::DefaultRecordParams.new(controller: controller, model: model)
65
+ end
66
+
67
+ def default_text_search
68
+ @default_text_search ||= ActiveElement::DefaultTextSearch.new(controller: controller, model: model)
69
+ end
70
+
71
+ def record_path(record, type = nil)
72
+ ActiveElement::Components::Util::RecordPath.new(record: record, controller: controller, type: type)
73
+ end
74
+
75
+ def namespace
76
+ controller.controller_path.rpartition('/').first.presence&.to_sym
77
+ end
78
+
79
+ def model
80
+ controller.controller_name.classify.constantize
81
+ end
82
+
83
+ def record
84
+ @record ||= model.find(controller.params[:id])
85
+ end
86
+
87
+ def collection
88
+ return model.all unless default_text_search.text_search?
89
+
90
+ model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
91
+ end
92
+ end
93
+ end
@@ -0,0 +1,62 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Provides params for ActiveRecord models when using the default boilerplate controller
5
+ # actions. Navigates input parameters and maps them to appropriate relations as needed.
6
+ class DefaultRecordParams
7
+ def initialize(controller:, model:)
8
+ @controller = controller
9
+ @model = model
10
+ end
11
+
12
+ def params
13
+ with_transformed_relations(
14
+ controller.params.require(controller.controller_name.singularize)
15
+ .permit(controller.active_element.state.fetch(:editable_fields, []))
16
+ )
17
+ end
18
+
19
+ private
20
+
21
+ attr_reader :controller, :model
22
+
23
+ def with_transformed_relations(params)
24
+ params.to_h.to_h do |key, value|
25
+ next [key, value] unless relation?(key)
26
+
27
+ relation_param(key, value)
28
+ end
29
+ end
30
+
31
+ def relation_param(key, value)
32
+ case relation(key).macro
33
+ when :belongs_to
34
+ belongs_to_param(key, value)
35
+ when :has_one
36
+ has_one_param(key, value)
37
+ when :has_many
38
+ has_many_param(key, value)
39
+ end
40
+ end
41
+
42
+ def belongs_to_param(key, value)
43
+ [relation(key).foreign_key, value]
44
+ end
45
+
46
+ def has_one_param(key, value) # rubocop:disable Naming/PredicateName
47
+ [relation(key).name, relation(key).klass.find_by(relation(key).klass.primary_key => value)]
48
+ end
49
+
50
+ def has_many_param(key, _value) # rubocop:disable Naming/PredicateName
51
+ [relation(key).name, relation(key).klass.where(relation(key).klass.primary_key => relation(key).value)]
52
+ end
53
+
54
+ def relation?(attribute)
55
+ relation(attribute.to_sym).present?
56
+ end
57
+
58
+ def relation(attribute)
59
+ model.reflect_on_association(attribute.to_sym)
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,110 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Full text search querying for DefaultController, provides full text search filters for all
5
+ # controllers with configured searchable fields. Includes support for querying across relations.
6
+ class DefaultTextSearch
7
+ def initialize(controller:, model:)
8
+ @controller = controller
9
+ @model = model
10
+ end
11
+
12
+ def search_filters
13
+ @search_filters ||= controller.params.permit(*searchable_fields).transform_values do |value|
14
+ value.try(:compact_blank) || value
15
+ end.compact_blank
16
+ end
17
+
18
+ def text_search?
19
+ search_filters.present?
20
+ end
21
+
22
+ def text_search
23
+ conditions = search_filters.to_h.map do |key, value|
24
+ next relation_matches(key, value) if relation?(key)
25
+ next datetime_between(key, value) if datetime?(key)
26
+
27
+ model.arel_table[key].matches("#{value}%")
28
+ end
29
+ conditions[1..].reduce(conditions.first) do |accumulated, condition|
30
+ accumulated.and(condition)
31
+ end
32
+ end
33
+
34
+ def search_relations
35
+ search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
36
+ end
37
+
38
+ private
39
+
40
+ attr_reader :controller, :model
41
+
42
+ def searchable_fields
43
+ base_fields = controller.active_element.state.fetch(:searchable_fields, [])
44
+ base_fields.map do |field|
45
+ next field unless field.to_s.end_with?('_at')
46
+
47
+ { field => %i[from to] }
48
+ end
49
+ end
50
+
51
+ def noop
52
+ Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
53
+ end
54
+
55
+ def datetime?(key)
56
+ model.columns.find { |column| column.name.to_s == key.to_s }&.type == :datetime
57
+ end
58
+
59
+ def datetime_between(key, value)
60
+ return noop if value[:from].blank? && value[:to].blank?
61
+
62
+ model.arel_table[key].between(range_begin(value)...range_end(value))
63
+ end
64
+
65
+ def range_begin(value)
66
+ value[:from].present? ? Time.zone.parse(value[:from]) + timezone_offset : -Float::INFINITY
67
+ end
68
+
69
+ def range_end
70
+ value[:to].present? ? Time.zone.parse(value[:to]) + timezone_offset : Float::INFINITY
71
+ end
72
+
73
+ def timezone_offset
74
+ controller.request.cookies['timezone_offset'].to_i.minutes
75
+ end
76
+
77
+ def relation_matches(key, value)
78
+ fields = searchable_relation_fields(key)
79
+ relation_model = relation(key).klass
80
+ fields.select! do |field|
81
+ relation_model.columns.find { |column| column.name.to_s == field.to_s }&.type == :string
82
+ end
83
+
84
+ return noop if fields.empty?
85
+
86
+ relation_conditions(fields, value, relation_model)
87
+ end
88
+
89
+ def relation_conditions(fields, value, relation_model)
90
+ fields[1..].reduce(relation_model.arel_table[fields.first].matches("#{value}%")) do |condition, field|
91
+ condition.or(relation_model.arel_table[field].matches("#{value}%"))
92
+ end
93
+ end
94
+
95
+ def searchable_relation_fields(key)
96
+ Components::Util.relation_controller(model, controller, key)
97
+ &.active_element
98
+ &.state
99
+ &.fetch(:searchable_fields, []) || []
100
+ end
101
+
102
+ def relation?(attribute)
103
+ relation(attribute.to_sym).present?
104
+ end
105
+
106
+ def relation(attribute)
107
+ model.reflect_on_association(attribute.to_sym)
108
+ end
109
+ end
110
+ end
@@ -0,0 +1,59 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ # Generates a schema for a JSON form field based on values stored in the database.
5
+ class JsonFieldSchema
6
+ def initialize(table:, column:)
7
+ @table = table
8
+ @column = column
9
+ end
10
+
11
+ def schema
12
+ data.map { |datum| structure(datum) }
13
+ end
14
+
15
+ private
16
+
17
+ attr_reader :table, :column, :initial_structure
18
+
19
+ def data
20
+ @data ||= ActiveRecord::Base.connection
21
+ .execute("select #{column} from #{table}")
22
+ .pluck(column)
23
+ .map { |datum| JSON.parse(datum) }
24
+ end
25
+
26
+ def structure(datum)
27
+ {
28
+ type: schema_type(datum),
29
+ shape: schema_shape(datum)&.compact,
30
+ fields: schema_fields(datum)&.compact
31
+ }
32
+ end
33
+
34
+ def schema_type(val)
35
+ case val
36
+ when Hash
37
+ 'object'
38
+ when Array
39
+ 'array'
40
+ when String
41
+ 'string'
42
+ end
43
+ end
44
+
45
+ def schema_shape(val)
46
+ return nil unless %w[array object].include?(schema_type(val))
47
+ return val.map { |item| structure(item).compact } if schema_type(val) == 'array'
48
+ return val.map { |_key, value| structure(value).compact } if schema_type(val) == 'object'
49
+
50
+ { type: schema_type(val) }
51
+ end
52
+
53
+ def schema_fields(val)
54
+ return nil unless schema_type(val) == 'object'
55
+
56
+ val.map { |key, value| { name: key }.merge(structure(value)).compact }
57
+ end
58
+ end
59
+ end
@@ -0,0 +1,98 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module PreRenderProcessors
5
+ # Selects fields from `__json_fields` param created by `Components::JsonField` and parses
6
+ # each field's JSON data back into request params to allow for transparent JSON data receipt.
7
+ # All params are permitted and converted to a Hash to allow them to be modified before
8
+ # converting back to ActionController::Params to avoid disrupting the Rails request flow.
9
+ class Json
10
+ def initialize(controller:)
11
+ @controller = controller
12
+ end
13
+
14
+ def process
15
+ return if json_fields.blank?
16
+
17
+ process_json_fields
18
+ delete_meta_params
19
+ rebuild_action_controller_parameters
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :controller
25
+
26
+ def process_json_fields
27
+ json_fields.zip(json_values).each do |field, value|
28
+ *nested_keys, field_key = field.split('.')
29
+ param = nested_keys.reduce(permitted_params) { |params, key| params[key] }
30
+ schema = schema_for(nested_keys + [field_key])
31
+ param[field_key] = coerced_with_default(value, schema)
32
+ end
33
+ end
34
+
35
+ def coerced_with_default(value, schema)
36
+ return coerced_value(JSON.parse(value), schema: schema) unless value == ''
37
+
38
+ { 'array' => [], 'object' => {} }.fetch(schema['type'])
39
+ end
40
+
41
+ def delete_meta_params
42
+ permitted_params.delete('__json_fields')
43
+ permitted_params.delete('__json_field_schemas')
44
+ end
45
+
46
+ def rebuild_action_controller_parameters
47
+ controller.params = ActionController::Parameters.new(permitted_params)
48
+ end
49
+
50
+ def json_fields
51
+ controller.params['__json_fields']
52
+ end
53
+
54
+ def json_values
55
+ json_fields.map do |json_field|
56
+ json_field.split('.').reduce(controller.params) { |params, field| params[field] }
57
+ end
58
+ end
59
+
60
+ def permitted_params
61
+ @permitted_params ||= controller.params.permit!.to_h
62
+ end
63
+
64
+ # rubocop:disable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
65
+ def coerced_value(val, schema:)
66
+ return val if val.nil?
67
+
68
+ case schema['type']
69
+ when 'array'
70
+ val.map { |item| coerced_value(item, schema: schema['shape']) }
71
+ when 'object'
72
+ val.to_h { |key, value| [key, coerced_value(value, schema: schema_field(schema, key))] }
73
+ when 'string', 'boolean', 'time'
74
+ val
75
+ when 'float'
76
+ Float(val)
77
+ when 'integer'
78
+ Integer(val)
79
+ when 'decimal'
80
+ BigDecimal(val)
81
+ when 'datetime'
82
+ DateTime.parse(val)
83
+ when 'date'
84
+ Date.parse(val)
85
+ end
86
+ end
87
+ # rubocop:enable Metrics/AbcSize, Metrics/CyclomaticComplexity, Metrics/MethodLength
88
+
89
+ def schema_for(path)
90
+ JSON.parse(path.reduce(permitted_params['__json_field_schemas']) { |schema, key| schema[key] })
91
+ end
92
+
93
+ def schema_field(schema, key)
94
+ schema['shape']['fields'].find { |each_field| each_field['name'] == key }
95
+ end
96
+ end
97
+ end
98
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'pre_render_processors/json'
4
+
5
+ module ActiveElement
6
+ # Collection of processors called before the controller flow is handed back to the host
7
+ # application, i.e. before actions within the main ActiveElement before action, e.g. used for
8
+ # processing JSON fields.
9
+ module PreRenderProcessors
10
+ end
11
+ end
@@ -36,6 +36,7 @@ module ActiveElement
36
36
  def primary?
37
37
  return false if rails_non_index_action?
38
38
  return false unless resourceless_get_request?
39
+ return false if excluded_ancestor?
39
40
 
40
41
  true
41
42
  end
@@ -93,6 +94,17 @@ module ActiveElement
93
94
  end
94
95
  end
95
96
 
97
+ def excluded_ancestor?
98
+ ancestors = controller.class.ancestors.map(&:name)
99
+ excluded_ancestors.any? { |excluded_ancestor| ancestors.include?(excluded_ancestor) }
100
+ end
101
+
102
+ def excluded_ancestors
103
+ # This will likely end up a config setting, for now we exclude Devise so its controllers
104
+ # don't appear in the Navbar.
105
+ %w[DeviseController]
106
+ end
107
+
96
108
  def permitted_action?
97
109
  permissions_check.permitted?
98
110
  rescue UnprotectedRouteError
@@ -33,7 +33,8 @@ module ActiveElement
33
33
 
34
34
  def available_routes
35
35
  @available_routes ||= descendants_with_permissions.map do |descendant, required_permissions|
36
- descendant.public_methods(false).map do |action|
36
+ action_methods = descendant.public_methods(false)
37
+ ([:index] + action_methods).uniq.map do |action|
37
38
  route(descendant, action, required_permissions)
38
39
  end
39
40
  end.flatten.compact.select(&:rails_route?).sort
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveElement
4
- VERSION = '0.0.10'
4
+ VERSION = '0.0.11'
5
5
  end