active_element 0.0.14 → 0.0.15

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 (42) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -23
  3. data/app/assets/javascripts/active_element/form.js +2 -1
  4. data/app/assets/javascripts/active_element/json_field.js +8 -1
  5. data/app/views/active_element/components/button.html.erb +6 -4
  6. data/app/views/active_element/components/form/_check_boxes.html.erb +6 -4
  7. data/app/views/active_element/components/form/_date_range_field.html.erb +14 -0
  8. data/app/views/active_element/components/form/_field.html.erb +3 -0
  9. data/app/views/active_element/components/form/_summary.html.erb +10 -0
  10. data/app/views/active_element/components/form.html.erb +12 -2
  11. data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
  12. data/app/views/active_element/components/table/_field.html.erb +2 -2
  13. data/app/views/active_element/components/table/collection.html.erb +20 -11
  14. data/app/views/active_element/components/table/item.html.erb +4 -0
  15. data/app/views/active_element/default_views/forbidden.html.erb +17 -3
  16. data/app/views/active_element/default_views/index.html.erb +14 -6
  17. data/config/locales/en.yml +3 -0
  18. data/example_app/.ruby-version +1 -1
  19. data/example_app/Gemfile +0 -2
  20. data/example_app/Gemfile.lock +5 -4
  21. data/lib/active_element/components/button.rb +6 -4
  22. data/lib/active_element/components/collection_table.rb +10 -3
  23. data/lib/active_element/components/form.rb +27 -2
  24. data/lib/active_element/components/item_table.rb +5 -3
  25. data/lib/active_element/components/navbar.rb +1 -1
  26. data/lib/active_element/components/util/association_mapping.rb +50 -26
  27. data/lib/active_element/components/util/default_display_value.rb +51 -0
  28. data/lib/active_element/components/util/display_value_mapping.rb +10 -0
  29. data/lib/active_element/components/util/field_mapping.rb +2 -2
  30. data/lib/active_element/components/util/form_field_mapping.rb +68 -15
  31. data/lib/active_element/components/util/record_mapping.rb +24 -3
  32. data/lib/active_element/components/util/record_path.rb +51 -20
  33. data/lib/active_element/components/util.rb +13 -0
  34. data/lib/active_element/controller_interface.rb +11 -1
  35. data/lib/active_element/controller_state.rb +4 -2
  36. data/lib/active_element/default_controller/params.rb +9 -1
  37. data/lib/active_element/default_controller/search.rb +22 -16
  38. data/lib/active_element/field_options.rb +20 -0
  39. data/lib/active_element/version.rb +1 -1
  40. data/lib/active_element.rb +1 -0
  41. data/rspec-documentation/pages/016-Default Controller.md +10 -1
  42. metadata +5 -2
@@ -16,6 +16,7 @@ module ActiveElement
16
16
 
17
17
  def link_tag
18
18
  verify_display_attribute
19
+ return nil if associated_record.blank?
19
20
  return associated_record.map { |value| link_to(value) } if multiple_association?
20
21
  return link_to(associated_record) if single_association?
21
22
  end
@@ -25,7 +26,7 @@ module ActiveElement
25
26
  when :has_one
26
27
  associated_record&.public_send(relation_key)
27
28
  when :has_many
28
- associated_record&.map(&relation_key)
29
+ associated_record&.map(&relation_key.to_sym)
29
30
  when :belongs_to
30
31
  record&.public_send(relation_key)
31
32
  end
@@ -47,6 +48,42 @@ module ActiveElement
47
48
  value.public_send(associated_model.primary_key)
48
49
  end
49
50
 
51
+ def display_field
52
+ @display_field ||= options.fetch(:attribute) do
53
+ next associated_model.default_display_attribute if defined_display_attribute?
54
+ next default_display_attribute if default_display_attribute.present?
55
+
56
+ associated_model.primary_key
57
+ end
58
+ end
59
+
60
+ def total_count
61
+ associated_model&.count
62
+ end
63
+
64
+ def options_for_select(scope: nil)
65
+ return [] if associated_model.blank?
66
+
67
+ base = scope.nil? ? associated_model : associated_model.public_send(scope)
68
+ base.all.pluck(display_field, associated_model.primary_key).sort.map do |title, value|
69
+ next [title, value] if display_field == associated_model.primary_key
70
+
71
+ ["#{title} (#{value})", value]
72
+ end
73
+ end
74
+
75
+ def single_association?
76
+ %i[has_one belongs_to].include?(relation.macro)
77
+ end
78
+
79
+ def multiple_association?
80
+ [:has_and_belongs_to_many, :has_many].include?(relation.macro)
81
+ end
82
+
83
+ def associated_model
84
+ record.association(field).klass
85
+ end
86
+
50
87
  private
51
88
 
52
89
  attr_reader :controller, :field, :record, :associated_record, :options
@@ -56,7 +93,7 @@ module ActiveElement
56
93
  end
57
94
 
58
95
  def associated_record_path(path_for)
59
- return nil unless controller.helpers.respond_to?(path_helper)
96
+ return nil if path_helper.nil?
60
97
 
61
98
  controller.helpers.public_send(path_helper, path_for)
62
99
  end
@@ -69,15 +106,6 @@ module ActiveElement
69
106
  "`#{associated_record.class.name}.default_display_attribute`"
70
107
  end
71
108
 
72
- def display_field
73
- @display_field ||= options.fetch(:attribute) do
74
- next associated_model.default_display_attribute if defined_display_attribute?
75
- next default_display_attribute if default_display_attribute.present?
76
-
77
- associated_model.primary_key
78
- end
79
- end
80
-
81
109
  def defined_display_attribute?
82
110
  associated_model.respond_to?(:default_display_attribute)
83
111
  end
@@ -91,7 +119,7 @@ module ActiveElement
91
119
  end
92
120
 
93
121
  def default_display_attribute
94
- %i[name email display_name username].find do |display_field|
122
+ %i[email name display_name username].find do |display_field|
95
123
  next true if associated_model_callable_method?(display_field.to_sym)
96
124
  next true if associated_model.columns.map(&:name).map(&:to_sym).include?(display_field.to_sym)
97
125
 
@@ -99,10 +127,6 @@ module ActiveElement
99
127
  end
100
128
  end
101
129
 
102
- def associated_model
103
- record.association(field).klass
104
- end
105
-
106
130
  def associated_model_callable_method?(name)
107
131
  return false unless associated_model.public_instance_methods.include?(name)
108
132
  return false unless associated_model.public_instance_method(name).arity.zero?
@@ -110,18 +134,18 @@ module ActiveElement
110
134
  true
111
135
  end
112
136
 
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
-
121
137
  def path_helper
122
- return "#{resource_name}_path" if namespace.blank?
138
+ names = [associated_record&.model_name&.singular].compact + Util.sti_record_names(associated_record)
139
+ names.compact.each do |name|
140
+ base_path = "#{name}_path"
141
+ namespace_path = "#{namespace}_#{name}_path" if namespace.present?
142
+ return base_path if namespace.blank? && controller.helpers.respond_to?(base_path)
143
+ return namespace_path if namespace_path.present? && controller.helpers.respond_to?(namespace_path)
144
+ end
123
145
 
124
- "#{namespace}_#{resource_name}_path"
146
+ nil
147
+ rescue
148
+ byebug
125
149
  end
126
150
 
127
151
  def link_to(value)
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Infers a default display value from any given object using multiple strategies.
7
+ class DefaultDisplayValue
8
+ DEFAULT_FIELDS = %i[display_name email name username].freeze
9
+
10
+ def initialize(object:)
11
+ @object = object
12
+ end
13
+
14
+ def value
15
+ DEFAULT_FIELDS.each do |field|
16
+ return object.public_send(field) if active_record_value?(field)
17
+ return object[field] if hash_key(field) if hash_value?(field)
18
+ end
19
+ end
20
+
21
+ private
22
+
23
+ attr_reader :object
24
+
25
+ def associated_model
26
+ object.model
27
+ end
28
+
29
+ def active_record_value?(field)
30
+ return false unless object.is_a?(ActiveRecord::Base)
31
+ return false unless object.respond_to?(field)
32
+ return false unless object.public_send(field).present?
33
+
34
+ true
35
+ end
36
+
37
+ def hash_value?(field)
38
+ return false unless object.respond_to?(:[])
39
+ return false unless object[field].present? || object[field.to_s].present?
40
+
41
+ true
42
+ end
43
+
44
+ def hash_key(field)
45
+ return field if object[field].present?
46
+ return field.to_s if object[field.to_s].present?
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -11,6 +11,12 @@ module ActiveElement
11
11
  association_mapping.link_tag
12
12
  end
13
13
 
14
+ def mapped_value_from_record
15
+ return value_record_path if value_from_record.is_a?(ActiveRecord::Base)
16
+
17
+ super
18
+ end
19
+
14
20
  def numeric_value
15
21
  value_from_record
16
22
  end
@@ -49,6 +55,10 @@ module ActiveElement
49
55
  require 'rgeo/geo_json'
50
56
  Util.json_pretty_print(RGeo::GeoJSON.encode(value_from_record))
51
57
  end
58
+
59
+ def value_record_path
60
+ RecordPath.new(record: value_from_record, controller: component.controller, type: :show).link
61
+ end
52
62
  end
53
63
  end
54
64
  end
@@ -73,8 +73,8 @@ module ActiveElement
73
73
  def default_value_mapper(field, options = nil)
74
74
  proc do |item|
75
75
  next default_record_value(field, item, options) if item.class.is_a?(ActiveModel::Naming)
76
- next item.public_send(field) if item.respond_to?(field)
77
- next item[field] if hash_field?(item, field)
76
+ next item.public_send(field) if item.class.is_a?(ActiveModel::Naming) && item.respond_to?(field)
77
+ next default_record_value(field, item, options) if hash_field?(item, field)
78
78
 
79
79
  nil
80
80
  end
@@ -10,9 +10,9 @@ module ActiveElement
10
10
  include EmailFields
11
11
 
12
12
  def initialize(record:, fields:, controller:, i18n:, search: false)
13
- @record = record
14
- @fields = fields
15
13
  @controller = controller
14
+ @record = record || default_record
15
+ @fields = fields
16
16
  @i18n = i18n
17
17
  @search = search
18
18
  end
@@ -20,7 +20,7 @@ module ActiveElement
20
20
  def fields_with_types_and_options
21
21
  compiled_fields = fields.map do |field|
22
22
  next field_with_default_type_and_default_options(field) unless field.is_a?(Array)
23
- next field if normalized_field?(field)
23
+ next field_with_provided_type_and_provided_options(field) if normalized_field?(field)
24
24
  next field_with_default_type_and_provided_options(field) if field_name_with_options?(field)
25
25
  next field_with_type(field) if field_name_with_type?(field)
26
26
 
@@ -47,18 +47,36 @@ module ActiveElement
47
47
  end
48
48
 
49
49
  def field_with_default_type_and_default_options(field)
50
+ return inline_configured_field(field) if inline_configuration?(field)
50
51
  return [field, type_from_file(field).to_sym, options_from_file(field)] if file_configuration?(field)
51
- return relation_text_search_field(field) if relation?(field) && record.present? && !search?
52
+ return relation_field(field) if relation?(field) && record.present?
52
53
 
53
54
  [field, default_type_from_model(field), default_options(field)]
54
55
  end
55
56
 
57
+ def inline_configuration?(field)
58
+ inline_configured_field(field).present?
59
+ end
60
+
61
+ def inline_configured_field(field)
62
+ field_options = FieldOptions.from_state(field, controller.active_element.state, record)
63
+ return nil if field_options.blank?
64
+
65
+ [field, field_options.type, field_options.options]
66
+ end
67
+
68
+ def field_with_provided_type_and_provided_options(field)
69
+ return relation_select_field(field.first) if relation?(field.first) && field[1] == :select
70
+
71
+ field
72
+ end
73
+
56
74
  def field_with_type(field)
57
- [field.first, field.last, default_options(field)]
75
+ [field.first, field.last, default_options(field.first)]
58
76
  end
59
77
 
60
78
  def association_mapping(field)
61
- @association_mapping ||= AssociationMapping.new(
79
+ AssociationMapping.new(
62
80
  controller: controller,
63
81
  field: field,
64
82
  record: record,
@@ -73,7 +91,7 @@ module ActiveElement
73
91
  #
74
92
  # active_element.component.form fields: [:foo, :bar, [:some_json_field, :text_area]]
75
93
  #
76
- file_configuration_path(field).file? && default_type_from_model(field) != :json_field
94
+ file_configuration_path(field).present? && default_type_from_model(field) != :json_field
77
95
  end
78
96
 
79
97
  def type_from_file(field)
@@ -86,7 +104,16 @@ module ActiveElement
86
104
  end
87
105
 
88
106
  def file_configuration_path(field)
89
- record_field_configuration_path(field) || sti_field_configuration_path(field)
107
+ file_configuration_paths(field).compact.find(&:file?)
108
+ end
109
+
110
+ def file_configuration_paths(field)
111
+ [
112
+ record_field_configuration_path(field),
113
+ sti_record_field_configuration_paths(field),
114
+ controller_field_configuration_path(field),
115
+ controller_field_configuration_path(field, scope: false)
116
+ ].flatten
90
117
  end
91
118
 
92
119
  def record_field_configuration_path(field)
@@ -96,11 +123,18 @@ module ActiveElement
96
123
  Rails.root.join('config/forms', record_name, "#{field}.yml")
97
124
  end
98
125
 
99
- def sti_record_field_configuration_path(field)
100
- sti_record_name = Util.sti_record_name(record)
101
- return nil if sti_record_name.blank?
126
+ def sti_record_field_configuration_paths(field)
127
+ sti_record_names = Util.sti_record_names(record)
128
+ return nil if sti_record_names.blank?
129
+
130
+ sti_record_names.map { |name| Rails.root.join('config/forms', name, "#{field}.yml") }
131
+ end
132
+
133
+ def controller_field_configuration_path(field, scope: true)
134
+ return nil if controller.blank?
102
135
 
103
- Rails.root.join('config/forms', sti_record_name, "#{field}.yml")
136
+ controller_segment = (scope ? controller.controller_path : controller.controller_name).singularize
137
+ Rails.root.join('config/forms', controller_segment, "#{field}.yml")
104
138
  end
105
139
 
106
140
  def default_type_from_model(field)
@@ -126,6 +160,20 @@ module ActiveElement
126
160
  model&.reflect_on_association(field)
127
161
  end
128
162
 
163
+ def relation_field(field)
164
+ return relation_text_search_field(field) if association_mapping(field).associated_model.count > 1000
165
+
166
+ relation_select_field(field)
167
+ end
168
+
169
+ def relation_select_field(field)
170
+ association = association_mapping(field)
171
+ columns = [association.display_field, association.associated_model.primary_key].compact
172
+ [association.relation_key, :select,
173
+ { multiple: association_mapping(field).multiple_association?,
174
+ options: association.associated_model.pluck(*columns) }]
175
+ end
176
+
129
177
  def relation_text_search_field(field)
130
178
  [field, :text_search_field,
131
179
  TextSearch.text_search_options(
@@ -136,9 +184,10 @@ module ActiveElement
136
184
  end
137
185
 
138
186
  def searchable_fields(field)
187
+ fields = Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || []
139
188
  # FIXME: Use database column type to only include strings/numbers.
140
- (Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || [])
141
- .reject { |searchable_field| searchable_field.to_s.end_with?('_at') }
189
+ searchable = fields.reject { |searchable_field| searchable_field.to_s.end_with?('_at') }
190
+ searchable.presence || [:id, :name].reject { |column| model.columns.map(&:name).include?(column) }
142
191
  end
143
192
 
144
193
  def relation_primary_key(field)
@@ -189,7 +238,7 @@ module ActiveElement
189
238
  return default_search_field_type(field) if search?
190
239
  return :password_field if secret_field?(field)
191
240
  return :email_field if email_field?(field)
192
- return :phone_field if phone_field?(field)
241
+ return :telephone_field if phone_field?(field)
193
242
 
194
243
  :text_field
195
244
  end
@@ -210,6 +259,10 @@ module ActiveElement
210
259
  }.merge(field_options(field))
211
260
  end
212
261
 
262
+ def default_record
263
+ controller&.controller_name&.classify&.safe_constantize&.new
264
+ end
265
+
213
266
  def required?(field)
214
267
  return false if record.blank?
215
268
  return false unless record.class.respond_to?(:validators)
@@ -17,7 +17,8 @@ module ActiveElement
17
17
  end
18
18
 
19
19
  def value
20
- return mapped_value_from_record if association? || column.present?
20
+ return value_from_config if value_from_config.present?
21
+ return mapped_value_from_record if active_record? || column.present?
21
22
 
22
23
  value_from_record
23
24
  end
@@ -36,16 +37,28 @@ module ActiveElement
36
37
  @column ||= record.class.columns.find { |model_column| model_column.name.to_s == field.to_s }
37
38
  end
38
39
 
40
+ def active_record?
41
+ association? || value_from_record.is_a?(ActiveRecord::Base)
42
+ end
43
+
39
44
  def association?
40
45
  return false unless record.is_a?(ActiveRecord::Base)
41
46
 
42
- record.association(field).present?
47
+ !record.association(field).nil?
43
48
  rescue ActiveRecord::AssociationNotFoundError
44
49
  false
45
50
  end
46
51
 
47
52
  def value_from_record
48
- record.public_send(field) if record.respond_to?(field)
53
+ return nil if field.blank?
54
+
55
+ @value_from_record ||= if record.respond_to?(field) && record.is_a?(ActiveRecord::Base)
56
+ record.public_send(field)
57
+ elsif record.respond_to?(:key?) && record.respond_to?(:[]) && record.key?(field)
58
+ record[field]
59
+ elsif record.respond_to?(field)
60
+ record.public_send(field)
61
+ end
49
62
  end
50
63
 
51
64
  def mapped_value_from_record
@@ -66,6 +79,14 @@ module ActiveElement
66
79
  )
67
80
  end
68
81
 
82
+ def value_from_config
83
+ field_options = FieldOptions.from_state(field, component.controller.active_element.state, record)
84
+ return nil if field_options.blank?
85
+ return nil unless DATABASE_TYPES.include?(field_options.type.to_sym)
86
+
87
+ send("#{field_options.type}_value")
88
+ end
89
+
69
90
  # Override these methods as required in a class that includes this module:
70
91
 
71
92
  def mapped_association_from_record
@@ -11,19 +11,27 @@ module ActiveElement
11
11
  @type = type&.to_sym || controller.action_name&.to_sym
12
12
  end
13
13
 
14
- def path
15
- record_path || sti_record_path
14
+ def path(**kwargs)
15
+ record_path(**kwargs)
16
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")}"
17
+ ActiveElement.warning("Unable to map #{record.inspect} to a Rails route (#{@controller.class.name}##{@type}). Tried:\n" \
18
+ "#{all_record_paths.join("\n")}")
19
+ nil
20
20
  end
21
21
 
22
- private
22
+ def link(**kwargs)
23
+ controller.helpers.link_to(DefaultDisplayValue.new(object: record).value, path(**kwargs))
24
+ end
23
25
 
24
- attr_reader :record, :controller, :type
26
+ def model
27
+ all_names.find do |name|
28
+ controller.helpers.public_send(record_path_for(name), *path_arguments)
29
+ rescue NoMethodError
30
+ nil
31
+ end&.classify&.constantize || default_model
32
+ end
25
33
 
26
- def namespace_prefix
34
+ def namespace
27
35
  # XXX: We guess the namespace from the current controller's module name. This will work
28
36
  # most of the time but will break if the current record's controller exists in a different
29
37
  # namespace to the current controller, e.g. `BackEndAdmin::UsersController` and
@@ -31,20 +39,45 @@ module ActiveElement
31
39
  # collection of `User` objects, the "show" path will be wrong:
32
40
  # `front_end_admin_user_path`. Maybe descend through the full controller class tree to
33
41
  # find a best match ?
34
- namespace = controller.class.name.deconstantize.underscore
42
+ controller.class.name.deconstantize.underscore.to_sym
43
+ end
44
+
45
+ private
46
+
47
+ def default_model
48
+ controller.controller_name.classify&.constantize
49
+ end
50
+
51
+ def all_names
52
+ @all_names ||= ([record_name] + sti_record_names).compact
53
+ end
54
+
55
+ def all_record_paths
56
+ @all_record_paths ||= all_names.map { |name| record_path_for(name) }
57
+ end
58
+
59
+ def namespace_prefix
35
60
  return nil if namespace.blank?
36
61
 
37
62
  "#{namespace}_"
38
63
  end
39
64
 
40
- def record_path
65
+ attr_reader :record, :controller, :type
66
+
67
+ def record_path(**kwargs) # rubocop:disable Metrics/AbcSize
41
68
  return nil if record.nil?
42
69
 
43
- controller.helpers.public_send(default_record_path, *path_arguments)
70
+ controller.helpers.public_send(default_record_path, *path_arguments, **kwargs)
44
71
  rescue NoMethodError
45
- raise NoMethodError if sti_record_name.nil?
72
+ raise if sti_record_names.blank?
46
73
 
47
- controller.helpers.public_send(sti_record_path, *path_arguments)
74
+ sti_record_names.each do |sti_record_name|
75
+ return controller.helpers.public_send(record_path_for(sti_record_name), *path_arguments, **kwargs)
76
+ rescue NoMethodError
77
+ nil
78
+ end
79
+
80
+ raise
48
81
  end
49
82
 
50
83
  def path_arguments
@@ -60,10 +93,8 @@ module ActiveElement
60
93
  "#{record_path_prefix}#{namespace_prefix}#{record_name}_path"
61
94
  end
62
95
 
63
- def sti_record_path
64
- return nil if sti_record_name.nil?
65
-
66
- "#{record_path_prefix}#{namespace_prefix}#{sti_record_name}_path"
96
+ def record_path_for(name)
97
+ "#{record_path_prefix}#{namespace_prefix}#{name}_path"
67
98
  end
68
99
 
69
100
  def record_name
@@ -72,10 +103,10 @@ module ActiveElement
72
103
  Util.record_name(record)&.pluralize
73
104
  end
74
105
 
75
- def sti_record_name
76
- return Util.sti_record_name(record) unless pluralize?
106
+ def sti_record_names
107
+ return Util.sti_record_names(record) unless pluralize?
77
108
 
78
- Util.sti_record_name(record)
109
+ Util.sti_record_names(record).map(&:pluralize)
79
110
  end
80
111
 
81
112
  def record_path_prefix
@@ -8,6 +8,7 @@ require_relative 'util/form_field_mapping'
8
8
  require_relative 'util/form_value_mapping'
9
9
  require_relative 'util/display_value_mapping'
10
10
  require_relative 'util/association_mapping'
11
+ require_relative 'util/default_display_value'
11
12
  require_relative 'util/decorator'
12
13
  require_relative 'util/numeric_field'
13
14
 
@@ -23,12 +24,24 @@ module ActiveElement
23
24
  record&.try(:model_name)&.try(&:singular) || default_record_name(record)
24
25
  end
25
26
 
27
+ # TODO: Remove and use .sti_record_names everywhere instead.
26
28
  def self.sti_record_name(record)
27
29
  return default_record_name(record) unless record.class.respond_to?(:inheritance_column)
28
30
 
29
31
  record&.class&.superclass&.model_name&.singular if record&.try(record.class.inheritance_column).present?
30
32
  end
31
33
 
34
+ def self.sti_record_names(record) # rubocop:disable Metrics/CyclomaticComplexity
35
+ record.class.ancestors.select do |ancestor|
36
+ next false if ancestor == record.class
37
+ next false if ancestor.try(:inheritance_column).blank?
38
+ next false unless ancestor < ActiveRecord::Base
39
+ next false if ancestor.abstract_class?
40
+
41
+ true
42
+ end.map(&:model_name).map(&:singular)
43
+ end
44
+
32
45
  def self.default_record_name(record)
33
46
  (record.is_a?(Class) ? record.name : record.class.name).demodulize.underscore
34
47
  end
@@ -38,10 +38,16 @@ module ActiveElement
38
38
  state.editable_fields.concat(args.map(&:to_sym)).uniq!
39
39
  end
40
40
 
41
- def searchable_fields(*args)
41
+ def searchable_fields(*args, required: false)
42
42
  state.searchable_fields.concat(args.map(&:to_sym)).uniq!
43
+ state.search_required = required
43
44
  end
44
45
 
46
+ def field_options(field, &block)
47
+ state.field_options[field] = block
48
+ end
49
+ alias field field_options
50
+
45
51
  def deletable
46
52
  state.deletable = true
47
53
  end
@@ -50,6 +56,10 @@ module ActiveElement
50
56
  RailsComponent.new(::Rails).application_name
51
57
  end
52
58
 
59
+ def t(identifier, **kwargs)
60
+ ::I18n.t("active_element.#{identifier}", **kwargs)
61
+ end
62
+
53
63
  def authenticate_with(&block)
54
64
  state.authenticator = block
55
65
  end
@@ -5,9 +5,10 @@ module ActiveElement
5
5
  # configuration. Used throughout ActiveElement for generating dynamic content based on
6
6
  # controller configuration.
7
7
  class ControllerState
8
- attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields
8
+ attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields,
9
+ :field_options
9
10
  attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
10
- :deletable, :authorizor, :authenticator, :list_order
11
+ :deletable, :authorizor, :authenticator, :list_order, :search_required
11
12
 
12
13
  def initialize(controller:)
13
14
  @controller = controller
@@ -19,6 +20,7 @@ module ActiveElement
19
20
  @viewable_fields = []
20
21
  @editable_fields = []
21
22
  @searchable_fields = []
23
+ @field_options = {}
22
24
  end
23
25
 
24
26
  def deletable?
@@ -33,7 +33,15 @@ module ActiveElement
33
33
  scalar, json = controller.active_element.state.editable_fields.partition do |field|
34
34
  scalar?(field)
35
35
  end
36
- scalar + [json_params(json)]
36
+ scalar + relation_fields + [json_params(json)]
37
+ end
38
+
39
+ def relation_fields
40
+ controller.active_element.state.editable_fields.map do |field|
41
+ next nil unless relation?(field)
42
+
43
+ relation(field).try(:foreign_key)
44
+ end.compact
37
45
  end
38
46
 
39
47
  def scalar?(field)