active_element 0.0.10 → 0.0.12

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 (219) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -2
  3. data/.strong_versions.yml +1 -0
  4. data/Gemfile +5 -0
  5. data/Gemfile.lock +115 -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 +38 -27
  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 +42 -24
  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 +8 -4
  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/_ungrouped_collection.html.erb +1 -0
  40. data/app/views/active_element/components/table/collection.html.erb +1 -1
  41. data/app/views/active_element/components/table/item.html.erb +6 -4
  42. data/app/views/active_element/default_views/edit.html.erb +5 -0
  43. data/app/views/active_element/default_views/forbidden.html.erb +7 -0
  44. data/app/views/active_element/default_views/index.html.erb +15 -0
  45. data/app/views/active_element/default_views/new.html.erb +4 -0
  46. data/app/views/active_element/default_views/show.html.erb +7 -0
  47. data/app/views/active_element/navbar/_menu.html.erb +1 -30
  48. data/app/views/active_element/theme/_select.html.erb +1 -1
  49. data/app/views/layouts/active_element.html.erb +16 -1
  50. data/config/brakeman.ignore +48 -0
  51. data/config/locales/en.yml +3 -0
  52. data/example_app/.gitattributes +7 -0
  53. data/example_app/.gitignore +35 -0
  54. data/example_app/.ruby-version +1 -0
  55. data/example_app/Gemfile +34 -0
  56. data/example_app/Gemfile.lock +296 -0
  57. data/example_app/README.md +24 -0
  58. data/example_app/Rakefile +6 -0
  59. data/example_app/app/assets/config/manifest.js +4 -0
  60. data/example_app/app/assets/images/.keep +0 -0
  61. data/example_app/app/assets/stylesheets/application.css +15 -0
  62. data/example_app/app/channels/application_cable/channel.rb +4 -0
  63. data/example_app/app/channels/application_cable/connection.rb +4 -0
  64. data/example_app/app/controllers/application_controller.rb +12 -0
  65. data/example_app/app/controllers/concerns/.keep +0 -0
  66. data/example_app/app/controllers/pets_controller.rb +7 -0
  67. data/example_app/app/controllers/users_controller.rb +7 -0
  68. data/example_app/app/helpers/application_helper.rb +2 -0
  69. data/example_app/app/javascript/application.js +3 -0
  70. data/example_app/app/javascript/controllers/application.js +9 -0
  71. data/example_app/app/javascript/controllers/hello_controller.js +7 -0
  72. data/example_app/app/javascript/controllers/index.js +11 -0
  73. data/example_app/app/jobs/application_job.rb +7 -0
  74. data/example_app/app/mailers/application_mailer.rb +4 -0
  75. data/example_app/app/models/application_record.rb +3 -0
  76. data/example_app/app/models/concerns/.keep +0 -0
  77. data/example_app/app/models/pet.rb +3 -0
  78. data/example_app/app/models/user.rb +8 -0
  79. data/example_app/app/views/layouts/application.html.erb +16 -0
  80. data/example_app/app/views/layouts/mailer.html.erb +13 -0
  81. data/example_app/app/views/layouts/mailer.text.erb +1 -0
  82. data/example_app/app/views/pets/index.html.erb +3 -0
  83. data/example_app/app/views/users/show.html.erb +3 -0
  84. data/example_app/bin/bundle +109 -0
  85. data/example_app/bin/importmap +4 -0
  86. data/example_app/bin/rails +4 -0
  87. data/example_app/bin/rake +4 -0
  88. data/example_app/bin/setup +33 -0
  89. data/example_app/config/application.rb +22 -0
  90. data/example_app/config/boot.rb +4 -0
  91. data/example_app/config/cable.yml +10 -0
  92. data/example_app/config/credentials.yml.enc +1 -0
  93. data/example_app/config/database.yml +25 -0
  94. data/example_app/config/environment.rb +5 -0
  95. data/example_app/config/environments/development.rb +70 -0
  96. data/example_app/config/environments/production.rb +93 -0
  97. data/example_app/config/environments/test.rb +60 -0
  98. data/example_app/config/importmap.rb +7 -0
  99. data/example_app/config/initializers/assets.rb +12 -0
  100. data/example_app/config/initializers/content_security_policy.rb +25 -0
  101. data/example_app/config/initializers/devise.rb +16 -0
  102. data/example_app/config/initializers/filter_parameter_logging.rb +8 -0
  103. data/example_app/config/initializers/inflections.rb +16 -0
  104. data/example_app/config/initializers/permissions_policy.rb +11 -0
  105. data/example_app/config/locales/devise.en.yml +65 -0
  106. data/example_app/config/locales/en.yml +33 -0
  107. data/example_app/config/puma.rb +43 -0
  108. data/example_app/config/routes.rb +8 -0
  109. data/example_app/config/storage.yml +34 -0
  110. data/example_app/config.ru +6 -0
  111. data/example_app/db/migrate/20230616210539_create_pet.rb +12 -0
  112. data/example_app/db/migrate/20230616211328_devise_create_users.rb +46 -0
  113. data/example_app/db/schema.rb +37 -0
  114. data/example_app/db/seeds.rb +33 -0
  115. data/example_app/lib/assets/.keep +0 -0
  116. data/example_app/lib/tasks/.keep +0 -0
  117. data/example_app/log/.keep +0 -0
  118. data/example_app/public/404.html +67 -0
  119. data/example_app/public/422.html +67 -0
  120. data/example_app/public/500.html +66 -0
  121. data/example_app/public/apple-touch-icon-precomposed.png +0 -0
  122. data/example_app/public/apple-touch-icon.png +0 -0
  123. data/example_app/public/favicon.ico +0 -0
  124. data/example_app/public/robots.txt +1 -0
  125. data/example_app/storage/.keep +0 -0
  126. data/example_app/test/application_system_test_case.rb +5 -0
  127. data/example_app/test/channels/application_cable/connection_test.rb +11 -0
  128. data/example_app/test/controllers/.keep +0 -0
  129. data/example_app/test/fixtures/files/.keep +0 -0
  130. data/example_app/test/fixtures/users.yml +11 -0
  131. data/example_app/test/helpers/.keep +0 -0
  132. data/example_app/test/integration/.keep +0 -0
  133. data/example_app/test/mailers/.keep +0 -0
  134. data/example_app/test/models/.keep +0 -0
  135. data/example_app/test/models/user_test.rb +7 -0
  136. data/example_app/test/system/.keep +0 -0
  137. data/example_app/test/test_helper.rb +13 -0
  138. data/example_app/tmp/.keep +0 -0
  139. data/example_app/tmp/pids/.keep +0 -0
  140. data/example_app/tmp/storage/.keep +0 -0
  141. data/example_app/vendor/.keep +0 -0
  142. data/example_app/vendor/javascript/.keep +0 -0
  143. data/lib/active_element/component.rb +9 -2
  144. data/lib/active_element/components/collection_table.rb +9 -2
  145. data/lib/active_element/components/email_fields.rb +14 -0
  146. data/lib/active_element/components/form.rb +48 -17
  147. data/lib/active_element/components/navbar.rb +64 -0
  148. data/lib/active_element/components/phone_fields.rb +14 -0
  149. data/lib/active_element/components/text_search/authorization.rb +9 -6
  150. data/lib/active_element/components/text_search/component.rb +4 -2
  151. data/lib/active_element/components/text_search.rb +13 -0
  152. data/lib/active_element/components/util/association_mapping.rb +74 -19
  153. data/lib/active_element/components/util/display_value_mapping.rb +13 -4
  154. data/lib/active_element/components/util/form_field_mapping.rb +139 -10
  155. data/lib/active_element/components/util/form_value_mapping.rb +3 -3
  156. data/lib/active_element/components/util/i18n.rb +1 -1
  157. data/lib/active_element/components/util/numeric_field.rb +73 -0
  158. data/lib/active_element/components/util/record_mapping.rb +43 -11
  159. data/lib/active_element/components/util/record_path.rb +21 -4
  160. data/lib/active_element/components/util.rb +13 -5
  161. data/lib/active_element/components.rb +3 -0
  162. data/lib/active_element/controller_action.rb +8 -2
  163. data/lib/active_element/controller_interface.rb +56 -18
  164. data/lib/active_element/controller_state.rb +44 -0
  165. data/lib/active_element/default_controller.rb +137 -0
  166. data/lib/active_element/default_record_params.rb +62 -0
  167. data/lib/active_element/default_search.rb +110 -0
  168. data/lib/active_element/json_field_schema.rb +59 -0
  169. data/lib/active_element/pre_render_processors/json.rb +98 -0
  170. data/lib/active_element/pre_render_processors.rb +11 -0
  171. data/lib/active_element/route.rb +12 -0
  172. data/lib/active_element/routes.rb +2 -1
  173. data/lib/active_element/version.rb +1 -1
  174. data/lib/active_element.rb +15 -32
  175. data/lib/tasks/active_element.rake +12 -1
  176. data/rspec-documentation/_head.html.erb +34 -0
  177. data/rspec-documentation/pages/000-Introduction.md +18 -0
  178. data/rspec-documentation/pages/005-Setup.md +75 -0
  179. data/rspec-documentation/pages/010-Components/Form Fields/Check Boxes.md +1 -0
  180. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Controller Params.md +97 -0
  181. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Schema.md +283 -0
  182. data/rspec-documentation/pages/010-Components/Form Fields/JSON/Types.md +36 -0
  183. data/rspec-documentation/pages/010-Components/Form Fields/JSON.md +70 -0
  184. data/rspec-documentation/pages/010-Components/Form Fields/Text Search.md +133 -0
  185. data/rspec-documentation/pages/010-Components/Form Fields.md +46 -0
  186. data/rspec-documentation/pages/010-Components/Forms.md +44 -0
  187. data/rspec-documentation/pages/010-Components/JSON Data.md +23 -0
  188. data/rspec-documentation/pages/010-Components/Navbar.md +56 -0
  189. data/rspec-documentation/pages/010-Components/Page Section Title.md +13 -0
  190. data/rspec-documentation/pages/010-Components/Page Subtitle.md +11 -0
  191. data/rspec-documentation/pages/010-Components/Page Title.md +11 -0
  192. data/rspec-documentation/pages/010-Components/Tables/Collection Table.md +29 -0
  193. data/rspec-documentation/pages/010-Components/Tables/Item Table.md +18 -0
  194. data/rspec-documentation/pages/010-Components/Tables/Options.md +19 -0
  195. data/rspec-documentation/pages/010-Components/Tables.md +29 -0
  196. data/rspec-documentation/pages/010-Components.md +15 -0
  197. data/rspec-documentation/pages/020-Access Control/010-Authentication.md +20 -0
  198. data/rspec-documentation/pages/020-Access Control/020-Authorization/Environments.md +9 -0
  199. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions/Custom Routes.md +41 -0
  200. data/rspec-documentation/pages/020-Access Control/020-Authorization/Permissions.md +58 -0
  201. data/rspec-documentation/pages/020-Access Control/020-Authorization/Setup.md +27 -0
  202. data/rspec-documentation/pages/020-Access Control/020-Authorization.md +11 -0
  203. data/rspec-documentation/pages/020-Access Control.md +31 -0
  204. data/rspec-documentation/pages/040-Decorators/Inline Decorators.md +24 -0
  205. data/rspec-documentation/pages/040-Decorators/View Decorators.md +55 -0
  206. data/rspec-documentation/pages/040-Decorators.md +12 -0
  207. data/rspec-documentation/pages/300-Alternatives.md +21 -0
  208. data/rspec-documentation/pages/900-License.md +11 -0
  209. data/rspec-documentation/spec_helper.rb +53 -16
  210. data/rspec-documentation/support.rb +84 -0
  211. metadata +159 -14
  212. data/rspec-documentation/pages/Components/Forms.md +0 -1
  213. data/rspec-documentation/pages/Components/Tables.md +0 -47
  214. data/rspec-documentation/pages/Components.md +0 -1
  215. data/rspec-documentation/pages/Decorators/Inline Decorators.md +0 -1
  216. data/rspec-documentation/pages/Decorators/View Decorators.md +0 -1
  217. data/rspec-documentation/pages/Index.md +0 -3
  218. data/rspec-documentation/pages/Util/I18n.md +0 -1
  219. /data/rspec-documentation/pages/{Components → 010-Components}/Tabs.md +0 -0
@@ -10,20 +10,21 @@ module ActiveElement
10
10
  attr_reader :controller
11
11
 
12
12
  # rubocop:disable Metrics/MethodLength
13
- def initialize(controller, fields:, submit:, item:, title: nil, destroy: false,
14
- modal: false, columns: 1, expanded: true, **kwargs)
13
+ def initialize(controller, fields:, submit:, item:, title: nil, destroy: false, search: false,
14
+ modal: false, columns: 1, **kwargs)
15
15
  @controller = controller
16
16
  @fields = fields
17
17
  @title = title
18
18
  @submit = submit
19
19
  @destroy = destroy
20
- @expanded = expanded
21
20
  @item = item
22
21
  @modal = modal
23
22
  @kwargs = kwargs
24
23
  @columns = columns
24
+ @search = search
25
25
  @action = kwargs.delete(:action) { default_action }
26
26
  @method = kwargs.delete(:method) { default_method }.to_s.downcase.to_sym
27
+ @tabindex = 1
27
28
  end
28
29
  # rubocop:enable Metrics/MethodLength
29
30
 
@@ -31,10 +32,10 @@ module ActiveElement
31
32
  'active_element/components/form'
32
33
  end
33
34
 
34
- def locals # rubocop:disable Metrics/MethodLength, Metrics/AbcSize
35
+ def locals # rubocop:disable Metrics/MethodLength
35
36
  {
36
37
  component: self,
37
- fields: Util::FormFieldMapping.new(record, fields, i18n).fields_with_types_and_options,
38
+ fields: form_field_mapping.fields_with_types_and_options,
38
39
  record: record,
39
40
  submit_label: submit_label,
40
41
  submit_position: submit_position,
@@ -44,7 +45,6 @@ module ActiveElement
44
45
  kwargs: kwargs,
45
46
  destroy: destroy,
46
47
  modal: modal,
47
- expanded: expanded,
48
48
  columns: columns,
49
49
  title: title,
50
50
  id: form_id
@@ -61,6 +61,10 @@ module ActiveElement
61
61
  base_options_for_select(field, field_options)
62
62
  end
63
63
 
64
+ def full_error_message
65
+ record&.errors&.full_messages&.join(', ')
66
+ end
67
+
64
68
  def valid?(field = nil)
65
69
  return true if record.blank? || !record.changed?
66
70
 
@@ -93,14 +97,18 @@ module ActiveElement
93
97
  end&.first
94
98
  end
95
99
 
96
- def value_for(field, default = nil)
97
- return form_value_mapping_value(field) if record.class.is_a?(ActiveModel::Naming)
98
- return default_record_value(field, default) if record.present? && record.respond_to?(field)
99
- return item[field].presence || default if item.present?
100
+ def value_for(*field, default: nil)
101
+ return form_value_mapping_value(field.first) if record.class.is_a?(ActiveModel::Naming)
102
+ return default_record_value(field.first, default) if record.present? && record.respond_to?(field.first)
103
+ return nested_value(field).presence || default if item.present?
100
104
 
101
105
  default
102
106
  end
103
107
 
108
+ def nested_value(field)
109
+ field.reduce(item) { |hash, key| hash.fetch(key, {}) }
110
+ end
111
+
104
112
  def form_value_mapping_value(field)
105
113
  Util::FormValueMapping.new(component: self, record: record, field: field).value
106
114
  end
@@ -114,7 +122,7 @@ module ActiveElement
114
122
  end
115
123
 
116
124
  def value_for_json_array_field(field, schema_field, element_index = nil)
117
- array = value_for(field, {}).fetch(schema_field[:name], [])
125
+ array = value_for(field, default: {}).fetch(schema_field[:name], [])
118
126
  return array if element_index.nil?
119
127
 
120
128
  array.fetch(element_index, nil)
@@ -130,13 +138,27 @@ module ActiveElement
130
138
  record&.class
131
139
  end
132
140
 
141
+ def tabindex
142
+ (@tabindex += 1)
143
+ end
144
+
133
145
  private
134
146
 
135
147
  attr_reader :fields, :submit, :title, :kwargs, :item, :method, :action,
136
- :destroy, :modal, :expanded, :columns
148
+ :destroy, :modal, :columns, :search
149
+
150
+ def form_field_mapping
151
+ @form_field_mapping ||= Util::FormFieldMapping.new(
152
+ record: record,
153
+ controller: controller,
154
+ fields: fields,
155
+ i18n: i18n,
156
+ search: search
157
+ )
158
+ end
137
159
 
138
160
  def valid_field?(field)
139
- return true if record.respond_to?("#{field}_changed?") && !record.public_send("#{field}_changed?")
161
+ return true if record.respond_to?(:changed?) && !record.changed?
140
162
 
141
163
  record&.errors.blank? || record.errors.full_messages_for(field).blank?
142
164
  end
@@ -157,7 +179,7 @@ module ActiveElement
157
179
 
158
180
  def submit_label_from_model
159
181
  return "Create #{humanized_model_name}" if record.present? && method == :post
160
- return "Update #{humanized_model_name}" if record.present? && %i[patch put].include?(method)
182
+ return 'Save Changes' if record.present? && %i[patch put].include?(method)
161
183
 
162
184
  nil
163
185
  end
@@ -189,7 +211,7 @@ module ActiveElement
189
211
  end
190
212
 
191
213
  def form_id
192
- kwargs.fetch(:id) { "form-#{SecureRandom.uuid}" }
214
+ kwargs.fetch(:id) { ActiveElement.element_id }
193
215
  end
194
216
 
195
217
  def autoformat(val, field_options)
@@ -209,10 +231,19 @@ module ActiveElement
209
231
  end
210
232
  end
211
233
 
234
+ def default_action_type
235
+ case controller.action_name
236
+ when 'new', 'create'
237
+ :create
238
+ when 'edit', 'update'
239
+ :update
240
+ end
241
+ end
242
+
212
243
  def default_action
213
- return controller.request.path unless record.is_a?(ActiveModel::Naming)
244
+ return controller.request.path unless record.class.is_a?(ActiveModel::Naming)
214
245
 
215
- Util::RecordPath.new(record: record, controller: controller).path
246
+ Util::RecordPath.new(record: record, controller: controller, type: default_action_type).path
216
247
  end
217
248
  end
218
249
  end
@@ -0,0 +1,64 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # A navigation bar providing links to areas within the application.
6
+ class Navbar
7
+ include Translations
8
+
9
+ attr_reader :controller
10
+
11
+ def initialize(controller, items: nil, fixed: true)
12
+ @controller = controller
13
+ @items = items
14
+ @fixed = fixed
15
+ end
16
+
17
+ def template
18
+ 'active_element/components/navbar'
19
+ end
20
+
21
+ def locals
22
+ {
23
+ component: self,
24
+ items: items,
25
+ fixed: fixed
26
+ }
27
+ end
28
+
29
+ def active_path_class(current_navbar_item:)
30
+ if ActiveMenuLink.new(
31
+ rails_component: RailsComponent.new(Rails),
32
+ navbar_items: items,
33
+ current_path: controller.request.path,
34
+ current_navbar_item: current_navbar_item,
35
+ controller_path: controller.controller_path,
36
+ action_name: controller.action_name
37
+ ).active?
38
+ 'active'
39
+ end
40
+ end
41
+
42
+ private
43
+
44
+ attr_reader :fixed
45
+
46
+ def items
47
+ return @items unless @items.nil?
48
+
49
+ ActiveElement.eager_load_controllers
50
+
51
+ @items ||= user_routes(controller.active_element.current_user).available.select(&:primary?).map do |route|
52
+ { path: route.path, title: route.title, spec: route.spec }
53
+ end
54
+ end
55
+
56
+ def user_routes(user)
57
+ ActiveElement::Routes.new(
58
+ permissions: user&.permissions,
59
+ rails_component: ActiveElement::RailsComponent.new(Rails)
60
+ )
61
+ end
62
+ end
63
+ end
64
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Provides a convenience method for detecting a field should be classified as a phone number.
6
+ module PhoneFields
7
+ PHONE_FIELDS = %w[phone telephone tel mobile].freeze
8
+
9
+ def phone_field?(field)
10
+ PHONE_FIELDS.any? { |phone_field| field.to_s.downcase.split('_').include?(phone_field) }
11
+ end
12
+ end
13
+ end
14
+ end
@@ -20,12 +20,12 @@ module ActiveElement
20
20
  end
21
21
  end
22
22
 
23
- def initialize(model:, params:, user:, search_columns:, result_columns:)
23
+ def initialize(model:, params:, user:, search_columns:, value_column:)
24
24
  @model = model
25
25
  @params = params
26
26
  @user = user
27
27
  @search_columns = search_columns
28
- @result_columns = result_columns
28
+ @value_column = value_column
29
29
  end
30
30
 
31
31
  def authorized?
@@ -45,14 +45,15 @@ module ActiveElement
45
45
 
46
46
  private
47
47
 
48
- attr_reader :model, :params, :user, :search_columns, :result_columns
48
+ attr_reader :model, :params, :user, :search_columns, :value_column
49
49
 
50
50
  def development_message
51
51
  paintbrush { green "Bypassed text search authorization in development environment: #{yellow message}" }
52
52
  end
53
53
 
54
54
  def missing_permissions
55
- (search_columns + result_columns).reject { |column| user_permitted?(column) }
55
+ (search_columns + [value_column]).reject { |column| user_permitted?(column) }
56
+ .compact_blank
56
57
  .map { |column| permission_for(column.name) }
57
58
  .uniq
58
59
  .sort
@@ -87,6 +88,8 @@ module ActiveElement
87
88
  end
88
89
 
89
90
  def user_permitted?(column)
91
+ return false if column.blank?
92
+
90
93
  user&.permissions&.include?(permission_for(column.name))
91
94
  end
92
95
 
@@ -99,10 +102,10 @@ module ActiveElement
99
102
  end
100
103
 
101
104
  def authorized_model?
102
- TextSearch.authorized_text_searches.any? do |authorized_model, search_fields, result_fields|
105
+ TextSearch.authorized_text_searches.any? do |authorized_model, search_fields, value_field|
103
106
  next false unless authorized_model == model
104
107
  next false unless authorized_fields?(search_columns, Array(search_fields))
105
- next false unless authorized_fields?(result_columns, Array(result_fields))
108
+ next false unless authorized_fields?(Array(value_column), Array(value_field))
106
109
 
107
110
  true
108
111
  end
@@ -92,7 +92,9 @@ module ActiveElement
92
92
  end
93
93
 
94
94
  def model
95
- @model ||= params[:model].camelize(:upper).safe_constantize
95
+ @model ||= ActiveRecord::Base.descendants.find do |descendant|
96
+ descendant.name == params[:model].camelize(:upper)
97
+ end
96
98
  end
97
99
 
98
100
  def authorization
@@ -101,7 +103,7 @@ module ActiveElement
101
103
  params: params,
102
104
  user: controller.active_element.current_user,
103
105
  search_columns: sql.search_columns.compact,
104
- result_columns: (sql.search_columns + [sql.value_column]).compact
106
+ value_column: sql.value_column
105
107
  )
106
108
  end
107
109
 
@@ -17,6 +17,19 @@ module ActiveElement
17
17
  def register_authorized_text_search(model:, with:, providing:)
18
18
  authorized_text_searches << [model, with, providing]
19
19
  end
20
+
21
+ def text_search_options(model:, with:, providing:)
22
+ {
23
+ search: { model: model.name.underscore, with: with, providing: providing },
24
+ placeholder: "Search for #{model.name.titleize} by #{humanized_names(with).join(', ')}..."
25
+ }
26
+ end
27
+
28
+ private
29
+
30
+ def humanized_names(names)
31
+ Array(names).compact.map.map(&:to_s).map(&:humanize)
32
+ end
20
33
  end
21
34
  end
22
35
  end
@@ -3,10 +3,11 @@
3
3
  module ActiveElement
4
4
  module Components
5
5
  module Util
6
- # Utility class for mapping an association to a linked URL (e.g. for displaying associations in a table).
6
+ # Utility class for mapping an association to a linked URL display value, (e.g. for
7
+ # displaying associations in a table, form, etc.).
7
8
  class AssociationMapping
8
- def initialize(component:, field:, record:, associated_record:, options:)
9
- @component = component
9
+ def initialize(controller:, field:, record:, associated_record:, options: {})
10
+ @controller = controller
10
11
  @field = field
11
12
  @record = record
12
13
  @associated_record = associated_record
@@ -15,19 +16,49 @@ module ActiveElement
15
16
 
16
17
  def link_tag
17
18
  verify_display_attribute
18
- return display_value if associated_record_path.blank?
19
+ return associated_record.map { |value| link_to(value) } if multiple_association?
20
+ return link_to(associated_record) if single_association?
21
+ end
19
22
 
20
- component.controller.helpers.link_to(display_value, associated_record_path)
23
+ def relation_id # rubocop:disable Metrics/CyclomaticComplexity
24
+ case relation.macro
25
+ when :has_one
26
+ associated_record&.public_send(relation_key)
27
+ when :has_many
28
+ associated_record&.map(&relation_key)
29
+ when :belongs_to
30
+ record&.public_send(relation_key)
31
+ end
32
+ end
33
+
34
+ def relation_key
35
+ case relation.macro
36
+ when :has_one, :has_many
37
+ relation.klass.primary_key
38
+ when :belongs_to
39
+ relation.foreign_key
40
+ end
41
+ end
42
+
43
+ def display_value(value = associated_record)
44
+ return value&.public_send(display_field) if display_field.present?
45
+ return nil if associated_model&.primary_key.blank?
46
+
47
+ value.public_send(associated_model.primary_key)
21
48
  end
22
49
 
23
50
  private
24
51
 
25
- attr_reader :component, :field, :record, :associated_record, :options
52
+ attr_reader :controller, :field, :record, :associated_record, :options
53
+
54
+ def relation
55
+ @relation ||= record.class.reflect_on_association(field)
56
+ end
26
57
 
27
- def associated_record_path
28
- return nil unless component.controller.helpers.respond_to?(path_helper)
58
+ def associated_record_path(path_for)
59
+ return nil unless controller.helpers.respond_to?(path_helper)
29
60
 
30
- component.controller.helpers.public_send(path_helper, associated_record)
61
+ controller.helpers.public_send(path_helper, path_for)
31
62
  end
32
63
 
33
64
  def verify_display_attribute
@@ -38,42 +69,66 @@ module ActiveElement
38
69
  "`#{associated_record.class.name}.default_display_attribute`"
39
70
  end
40
71
 
41
- def display_value
42
- associated_record.public_send(display_field)
43
- end
44
-
45
72
  def display_field
46
73
  @display_field ||= options.fetch(:attribute) do
47
- next associated_record.class.default_display_attribute if defined_display_attribute?
74
+ next associated_model.default_display_attribute if defined_display_attribute?
48
75
  next default_display_attribute if default_display_attribute.present?
49
76
 
50
- associated_record.class.primary_key
77
+ associated_model.primary_key
51
78
  end
52
79
  end
53
80
 
54
81
  def defined_display_attribute?
55
- associated_record.class.respond_to?(:default_display_attribute)
82
+ associated_model.respond_to?(:default_display_attribute)
56
83
  end
57
84
 
58
85
  def namespace
59
- component.controller.class.name.deconstantize.underscore
86
+ controller.class.name.deconstantize.underscore
60
87
  end
61
88
 
62
89
  def resource_name
63
- record.public_send(field).model_name.singular
90
+ record.public_send(field)&.model_name&.singular
64
91
  end
65
92
 
66
93
  def default_display_attribute
67
94
  %i[name email display_name username].find do |display_field|
68
- associated_record.respond_to?(display_field) && associated_record.method(display_field).arity.zero?
95
+ next true if associated_model_callable_method?(display_field.to_sym)
96
+ next true if associated_model.columns.map(&:name).map(&:to_sym).include?(display_field.to_sym)
97
+
98
+ false
69
99
  end
70
100
  end
71
101
 
102
+ def associated_model
103
+ record.association(field).klass
104
+ end
105
+
106
+ def associated_model_callable_method?(name)
107
+ return false unless associated_model.public_instance_methods.include?(name)
108
+ return false unless associated_model.public_instance_method(name).arity.zero?
109
+
110
+ true
111
+ end
112
+
113
+ def single_association?
114
+ %i[has_one belongs_to].include?(relation.macro)
115
+ end
116
+
117
+ def multiple_association?
118
+ relation.macro == :has_many
119
+ end
120
+
72
121
  def path_helper
73
122
  return "#{resource_name}_path" if namespace.blank?
74
123
 
75
124
  "#{namespace}_#{resource_name}_path"
76
125
  end
126
+
127
+ def link_to(value)
128
+ return display_value(value) if associated_record_path(value).blank?
129
+
130
+ controller.helpers.link_to(display_value(value), associated_record_path(value))
131
+ end
77
132
  end
78
133
  end
79
134
  end
@@ -7,12 +7,21 @@ module ActiveElement
7
7
  class DisplayValueMapping
8
8
  include RecordMapping
9
9
 
10
+ def mapped_association_from_record
11
+ association_mapping.link_tag
12
+ end
13
+
10
14
  def numeric_value
11
15
  value_from_record
12
16
  end
13
17
 
14
18
  def json_value
15
- ActiveElement.json_pretty_print(value_from_record)
19
+ return ActiveElement.json_pretty_print(value_from_record) unless component.is_a?(CollectionTable)
20
+
21
+ component.controller.render_to_string(
22
+ partial: 'active_element/components/fields/json',
23
+ locals: { value: value_from_record, field_id: ActiveElement.element_id }
24
+ )
16
25
  end
17
26
 
18
27
  def string_value
@@ -20,15 +29,15 @@ module ActiveElement
20
29
  end
21
30
 
22
31
  def datetime_value
23
- value_from_record.strftime('%Y-%m-%d %H:%M:%S')
32
+ with_timezone_offset(value_from_record).strftime('%Y-%m-%d %H:%M:%S')
24
33
  end
25
34
 
26
35
  def time_value
27
- value_from_record.strftime('%H:%M:%S')
36
+ with_timezone_offset(value_from_record).strftime('%H:%M:%S')
28
37
  end
29
38
 
30
39
  def date_value
31
- value_from_record.strftime('%Y-%m-%d')
40
+ with_timezone_offset(value_from_record).strftime('%Y-%m-%d')
32
41
  end
33
42
 
34
43
  def boolean_value