active_element 0.0.10 → 0.0.12

Sign up to get free protection for your applications and to get access to all the features.
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