active_element 0.0.1 → 0.0.3

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 (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/.strong_versions.yml +2 -0
  4. data/Gemfile +10 -2
  5. data/Gemfile.lock +229 -4
  6. data/Rakefile +1 -0
  7. data/active_element.gemspec +7 -0
  8. data/app/assets/config/active_element/manifest.js +2 -0
  9. data/app/assets/javascripts/active_element/application.js +10 -0
  10. data/app/assets/javascripts/active_element/confirm.js +67 -0
  11. data/app/assets/javascripts/active_element/form.js +61 -0
  12. data/app/assets/javascripts/active_element/json_field.js +316 -0
  13. data/app/assets/javascripts/active_element/pagination.js +18 -0
  14. data/app/assets/javascripts/active_element/search_field.js +127 -0
  15. data/app/assets/javascripts/active_element/secret.js +40 -0
  16. data/app/assets/javascripts/active_element/setup.js +36 -0
  17. data/app/assets/javascripts/active_element/theme.js +42 -0
  18. data/app/assets/stylesheets/active_element/_variables.scss +142 -0
  19. data/app/assets/stylesheets/active_element/application.scss +77 -0
  20. data/app/controllers/active_element/application_controller.rb +41 -0
  21. data/app/controllers/active_element/text_searches_controller.rb +189 -0
  22. data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
  23. data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
  24. data/app/views/active_element/components/button.html.erb +27 -0
  25. data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
  26. data/app/views/active_element/components/form/_check_box.html.erb +3 -0
  27. data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
  28. data/app/views/active_element/components/form/_field.html.erb +28 -0
  29. data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
  30. data/app/views/active_element/components/form/_json.html.erb +12 -0
  31. data/app/views/active_element/components/form/_label.html.erb +17 -0
  32. data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
  33. data/app/views/active_element/components/form/_select.html.erb +4 -0
  34. data/app/views/active_element/components/form/_summary.html.erb +40 -0
  35. data/app/views/active_element/components/form/_templates.html.erb +85 -0
  36. data/app/views/active_element/components/form/_text_area.html.erb +4 -0
  37. data/app/views/active_element/components/form/_text_search.html.erb +16 -0
  38. data/app/views/active_element/components/form.html.erb +78 -0
  39. data/app/views/active_element/components/json.html.erb +8 -0
  40. data/app/views/active_element/components/page_description.html.erb +3 -0
  41. data/app/views/active_element/components/secret/_field.html.erb +1 -0
  42. data/app/views/active_element/components/secret/_templates.html.erb +11 -0
  43. data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
  44. data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
  45. data/app/views/active_element/components/table/_pagination.html.erb +17 -0
  46. data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
  47. data/app/views/active_element/components/table/collection.html.erb +39 -0
  48. data/app/views/active_element/components/table/item.html.erb +39 -0
  49. data/app/views/active_element/components/tabs.html.erb +7 -0
  50. data/app/views/active_element/decorators/_boolean.html.erb +5 -0
  51. data/app/views/active_element/decorators/_date.html.erb +3 -0
  52. data/app/views/active_element/decorators/_datetime.html.erb +3 -0
  53. data/app/views/active_element/decorators/_time.html.erb +3 -0
  54. data/app/views/active_element/forbidden.html.erb +33 -0
  55. data/app/views/active_element/main_menu/_item.html.erb +9 -0
  56. data/app/views/active_element/navbar/_menu.html.erb +30 -0
  57. data/app/views/active_element/theme/_select.html.erb +1 -0
  58. data/app/views/active_element/theme/_templates.html.erb +6 -0
  59. data/app/views/kaminari/_first_page.html.erb +3 -0
  60. data/app/views/kaminari/_gap.html.erb +3 -0
  61. data/app/views/kaminari/_last_page.html.erb +3 -0
  62. data/app/views/kaminari/_next_page.html.erb +3 -0
  63. data/app/views/kaminari/_page.html.erb +9 -0
  64. data/app/views/kaminari/_paginator.html.erb +17 -0
  65. data/app/views/kaminari/_prev_page.html.erb +3 -0
  66. data/app/views/layouts/active_element.html.erb +65 -0
  67. data/app/views/layouts/active_element_error.html.erb +40 -0
  68. data/config/routes.rb +5 -0
  69. data/lib/active_element/active_menu_link.rb +80 -0
  70. data/lib/active_element/active_record_text_search_authorization.rb +12 -0
  71. data/lib/active_element/colorized_string.rb +33 -0
  72. data/lib/active_element/component.rb +122 -0
  73. data/lib/active_element/components/button.rb +156 -0
  74. data/lib/active_element/components/collection_table.rb +118 -0
  75. data/lib/active_element/components/form.rb +210 -0
  76. data/lib/active_element/components/item_table.rb +57 -0
  77. data/lib/active_element/components/json.rb +31 -0
  78. data/lib/active_element/components/link_helpers.rb +9 -0
  79. data/lib/active_element/components/page_description.rb +28 -0
  80. data/lib/active_element/components/secret_fields.rb +15 -0
  81. data/lib/active_element/components/tab.rb +37 -0
  82. data/lib/active_element/components/tabs.rb +35 -0
  83. data/lib/active_element/components/translations.rb +18 -0
  84. data/lib/active_element/components/util/association_mapping.rb +80 -0
  85. data/lib/active_element/components/util/decorator.rb +107 -0
  86. data/lib/active_element/components/util/display_value_mapping.rb +48 -0
  87. data/lib/active_element/components/util/field_mapping.rb +144 -0
  88. data/lib/active_element/components/util/form_field_mapping.rb +104 -0
  89. data/lib/active_element/components/util/form_value_mapping.rb +49 -0
  90. data/lib/active_element/components/util/i18n.rb +66 -0
  91. data/lib/active_element/components/util/record_mapping.rb +111 -0
  92. data/lib/active_element/components/util.rb +43 -0
  93. data/lib/active_element/components.rb +20 -0
  94. data/lib/active_element/controller_action.rb +91 -0
  95. data/lib/active_element/engine.rb +26 -0
  96. data/lib/active_element/permissions_check.rb +101 -0
  97. data/lib/active_element/rails_component.rb +40 -0
  98. data/lib/active_element/route.rb +112 -0
  99. data/lib/active_element/routes.rb +62 -0
  100. data/lib/active_element/version.rb +1 -1
  101. data/lib/active_element.rb +91 -1
  102. data/lib/tasks/active_element.rake +23 -0
  103. data/rspec-documentation/dummy +1 -0
  104. data/rspec-documentation/pages/Components/Forms.md +1 -0
  105. data/rspec-documentation/pages/Components/Tables.md +47 -0
  106. data/rspec-documentation/pages/Components/Tabs.md +1 -0
  107. data/rspec-documentation/pages/Components.md +1 -0
  108. data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
  109. data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
  110. data/rspec-documentation/pages/Index.md +3 -0
  111. data/rspec-documentation/pages/Util/I18n.md +1 -0
  112. data/rspec-documentation/spec_helper.rb +35 -0
  113. metadata +191 -3
@@ -0,0 +1,210 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # A form component for rendering a standard form with various inputs in a uniform manner,
6
+ # includes validation errors.
7
+ class Form # rubocop:disable Metrics/ClassLength
8
+ include Translations
9
+
10
+ attr_reader :controller
11
+
12
+ # rubocop:disable Metrics/MethodLength
13
+ def initialize(controller, fields:, submit:, item:, title: nil, destroy: false,
14
+ modal: false, columns: 1, expanded: true, **kwargs)
15
+ @controller = controller
16
+ @fields = fields
17
+ @title = title
18
+ @submit = submit
19
+ @destroy = destroy
20
+ @expanded = expanded
21
+ @item = item
22
+ @modal = modal
23
+ @kwargs = kwargs
24
+ @columns = columns
25
+ @method = kwargs.delete(:method) { default_method }.to_s.downcase.to_sym
26
+ end
27
+ # rubocop:enable Metrics/MethodLength
28
+
29
+ def template
30
+ 'active_element/components/form'
31
+ end
32
+
33
+ def locals # rubocop:disable Metrics/MethodLength
34
+ {
35
+ component: self,
36
+ fields: Util::FormFieldMapping.new(record, fields, i18n).fields_with_types_and_options,
37
+ record: record,
38
+ submit_label: submit_label,
39
+ submit_position: submit_position,
40
+ class_name: class_name,
41
+ method: method,
42
+ kwargs: kwargs,
43
+ destroy: destroy,
44
+ modal: modal,
45
+ expanded: expanded,
46
+ columns: columns,
47
+ title: title,
48
+ id: form_id
49
+ }
50
+ end
51
+
52
+ def class_name
53
+ [default_class_name, kwargs.fetch(:class, nil)].compact.join(' ')
54
+ end
55
+
56
+ def options_for_select(field, field_options)
57
+ return [['', '']] + base_options_for_select(field, field_options) unless field_options[:blank] == false
58
+
59
+ base_options_for_select(field, field_options)
60
+ end
61
+
62
+ def valid?(field = nil)
63
+ return true if record.blank? || !record.changed?
64
+
65
+ record.valid?
66
+
67
+ return record&.errors.blank? if field.nil?
68
+
69
+ valid_field?(field)
70
+ end
71
+
72
+ def name_for(form, schema_field, type:)
73
+ base = "#{form.object_name}[#{schema_field.fetch(:name)}]"
74
+ type == :array ? "#{base}[]" : base
75
+ end
76
+
77
+ def schema_for(field, options)
78
+ options.key?(:schema) ? options.fetch(:schema) : schema_from_yaml(field)
79
+ end
80
+
81
+ def schema_from_yaml(field)
82
+ YAML.safe_load(
83
+ Rails.root.join("config/forms/#{record.class.name.underscore}/#{field}.yml").read,
84
+ symbolize_names: true
85
+ )
86
+ end
87
+
88
+ def display_value_for_select(field, options)
89
+ options_for_select(field, options).find do |_display_value, value|
90
+ value == value_for(field)
91
+ end&.first
92
+ end
93
+
94
+ def value_for(field, default = nil)
95
+ return form_value_mapping_value(field) if record.class.is_a?(ActiveModel::Naming)
96
+ return default_record_value(field, default) if record.present? && record.respond_to?(field)
97
+ return item[field].presence || default if item.present?
98
+
99
+ default
100
+ end
101
+
102
+ def form_value_mapping_value(field)
103
+ Util::FormValueMapping.new(component: self, record: record, field: field).value
104
+ end
105
+
106
+ def default_record_value(field, default)
107
+ record&.public_send(field).presence || default
108
+ end
109
+
110
+ def options_for_json_array_field(options)
111
+ options.map { |option| option.is_a?(Array) ? option : [option, option] }
112
+ end
113
+
114
+ def value_for_json_array_field(field, schema_field, element_index = nil)
115
+ array = value_for(field, {}).fetch(schema_field[:name], [])
116
+ return array if element_index.nil?
117
+
118
+ array.fetch(element_index, nil)
119
+ end
120
+
121
+ def record
122
+ return nil if kwargs.fetch(:model, nil).blank?
123
+
124
+ kwargs[:model].is_a?(Array) ? kwargs[:model].last : kwargs[:model]
125
+ end
126
+
127
+ def model
128
+ record&.class
129
+ end
130
+
131
+ private
132
+
133
+ attr_reader :fields, :submit, :title, :kwargs, :item, :method, :destroy, :modal, :expanded, :columns
134
+
135
+ def valid_field?(field)
136
+ return true if record.respond_to?("#{field}_changed?") && !record.public_send("#{field}_changed?")
137
+
138
+ record&.errors.blank? || record.errors.full_messages_for(field).blank?
139
+ end
140
+
141
+ def submit_position
142
+ return submit[:position] if submit.is_a?(Hash) && submit[:position].present?
143
+
144
+ :bottom
145
+ end
146
+
147
+ def submit_label
148
+ return submit[:label] if submit.is_a?(Hash) && submit[:label].present?
149
+ return submit if submit.is_a?(String)
150
+ return submit_label_from_model if record.present? && submit_label_from_model.present?
151
+
152
+ 'Submit'
153
+ end
154
+
155
+ def submit_label_from_model
156
+ return "Create #{humanized_model_name}" if record.present? && method == :post
157
+ return "Update #{humanized_model_name}" if record.present? && %i[patch put].include?(method)
158
+
159
+ nil
160
+ end
161
+
162
+ def humanized_model_name
163
+ record.class.name.titleize
164
+ end
165
+
166
+ def base_options_for_select(field, field_options)
167
+ return normalized_options(field_options.fetch(:options)) if field_options.key?(:options)
168
+ return default_options_for_select(field, field_options) if record.class.is_a?(ActiveModel::Naming)
169
+
170
+ raise ArgumentError, "Must provide select options `[:#{field}, { options: [...] }]` or a record instance."
171
+ end
172
+
173
+ def normalized_options(options)
174
+ options.map { |option| option.is_a?(Array) ? option : [option, option] }
175
+ end
176
+
177
+ def default_class_name
178
+ return nil if record.blank?
179
+
180
+ Util::I18n.class_name(record.class.name)
181
+ end
182
+
183
+ def default_options_for_select(field, field_options)
184
+ options = record.class.distinct.order(field).pluck(field)
185
+ [options.map { |option| autoformat(option, field_options) }, options].transpose
186
+ end
187
+
188
+ def form_id
189
+ kwargs.fetch(:id) { "form-#{SecureRandom.uuid}" }
190
+ end
191
+
192
+ def autoformat(val, field_options)
193
+ return val unless field_options[:autoformat] || !field_options.key?(:autoformat)
194
+
195
+ val.titleize
196
+ end
197
+
198
+ def default_method
199
+ case controller.action_name
200
+ when 'edit'
201
+ 'PATCH'
202
+ when 'index'
203
+ 'GET'
204
+ else
205
+ 'POST'
206
+ end
207
+ end
208
+ end
209
+ end
210
+ end
@@ -0,0 +1,57 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # A table component for rendering the fields of a single object horizontally.
6
+ class ItemTable
7
+ include LinkHelpers
8
+ include SecretFields
9
+
10
+ attr_reader :controller, :model_name
11
+
12
+ def initialize(controller, item:, fields:, class_name: nil, model_name: nil,
13
+ edit: false, new: false, destroy: false, style: nil, row_class: nil, **_kwargs)
14
+ @controller = controller
15
+ @class_name = class_name
16
+ @model_name = model_name
17
+ @item = item
18
+ @fields = fields
19
+ @destroy = destroy
20
+ @edit = edit
21
+ @new = new
22
+ @style = style
23
+ @row_class = row_class
24
+ end
25
+
26
+ def template
27
+ 'active_element/components/table/item'
28
+ end
29
+
30
+ def locals # rubocop:disable Metrics/MethodLength
31
+ {
32
+ component: self,
33
+ class_name: class_name,
34
+ item: item,
35
+ fields: Util::FieldMapping.new(self, fields, class_name).mapped_fields,
36
+ destroy: destroy,
37
+ edit: edit,
38
+ new: new,
39
+ style: style,
40
+ row_class_mapper: row_class_mapper
41
+ }
42
+ end
43
+
44
+ def model
45
+ item.class.is_a?(ActiveModel::Naming) ? item.class : nil
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :class_name, :item, :fields, :edit, :new, :destroy, :style, :row_class
51
+
52
+ def row_class_mapper
53
+ row_class.is_a?(Proc) ? row_class : proc { row_class }
54
+ end
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Outputs a `<script>` tag and sets globally-available JSON data, available as `ActiveElement.jsonData.<key>`.
6
+ # Note key is camelized, so `foo_bar_baz` becomes `fooBarBaz`.
7
+ class Json
8
+ def initialize(controller, object:, key:)
9
+ @controller = controller
10
+ @object = object
11
+ @key = key
12
+ end
13
+
14
+ def template
15
+ 'active_element/components/json'
16
+ end
17
+
18
+ def locals
19
+ {
20
+ controller: controller,
21
+ object: object,
22
+ key: ActiveElement::Components::Util.json_name(key)
23
+ }
24
+ end
25
+
26
+ private
27
+
28
+ attr_reader :controller, :object, :key
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,9 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Provides helpers for generating links to show, destroy, etc. actions.
6
+ module LinkHelpers
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,28 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Provides a description of a page, intended to be used underneath the `page_title` component.
6
+ class PageDescription
7
+ def initialize(controller, content:)
8
+ @controller = controller
9
+ @content = content
10
+ end
11
+
12
+ def template
13
+ 'active_element/components/page_description'
14
+ end
15
+
16
+ def locals
17
+ {
18
+ component: self,
19
+ content: content
20
+ }
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :controller, :content
26
+ end
27
+ end
28
+ end
@@ -0,0 +1,15 @@
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 secret, used
6
+ # for censoring values or selecting the default form field type as `password_field`, etc.
7
+ module SecretFields
8
+ SECRET_FIELDS = %w[secret password].freeze
9
+
10
+ def secret_field?(field)
11
+ SECRET_FIELDS.any? { |secret_field| field.to_s.downcase.include?(secret_field) }
12
+ end
13
+ end
14
+ end
15
+ end
@@ -0,0 +1,37 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # One navigation tab in a Tabs component.
6
+ class Tab
7
+ attr_reader :title, :path, :content
8
+
9
+ def initialize(controller, title:, path:, &block)
10
+ @controller = controller
11
+ @title = title
12
+ @path = path
13
+ @content = block_given? ? block.call : ''
14
+ end
15
+
16
+ def selected?
17
+ controller.request.fullpath == path
18
+ end
19
+
20
+ def identifier
21
+ Util::I18n.class_name(title)
22
+ end
23
+
24
+ def to_s
25
+ ''
26
+ end
27
+
28
+ def tab(title)
29
+ yield Tab.new(title)
30
+ end
31
+
32
+ private
33
+
34
+ attr_reader :controller
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Navigation tabs component.
6
+ class Tabs
7
+ def initialize(controller, class_name:)
8
+ @controller = controller
9
+ @class_name = class_name
10
+ @tabs = []
11
+ yield self
12
+ end
13
+
14
+ def to_s
15
+ ''
16
+ end
17
+
18
+ def tab(title:, path:, &block)
19
+ Tab.new(controller, title: title, path: path, &block).tap { |tab| @tabs << tab }
20
+ end
21
+
22
+ def template
23
+ 'active_element/components/tabs'
24
+ end
25
+
26
+ def locals
27
+ { tabs: tabs, class_name: class_name }
28
+ end
29
+
30
+ private
31
+
32
+ attr_reader :tabs, :controller, :class_name
33
+ end
34
+ end
35
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ # Provides `#i18n` method as standard entrypoint to translation point, specifies required
6
+ # interface for classes that use this module.
7
+ module Translations
8
+ def i18n
9
+ @i18n ||= Util::I18n.new(self)
10
+ end
11
+
12
+ def model
13
+ raise NotImplementedError,
14
+ 'Component must implement `#model` and return `nil` or an ActiveRecord model class.'
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Utility class for mapping an association to a linked URL (e.g. for displaying associations in a table).
7
+ class AssociationMapping
8
+ def initialize(component:, field:, record:, associated_record:, options:)
9
+ @component = component
10
+ @field = field
11
+ @record = record
12
+ @associated_record = associated_record
13
+ @options = options || {}
14
+ end
15
+
16
+ def link_tag
17
+ verify_display_attribute
18
+ return display_value if associated_record_path.blank?
19
+
20
+ component.controller.helpers.link_to(display_value, associated_record_path)
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :component, :field, :record, :associated_record, :options
26
+
27
+ def associated_record_path
28
+ return nil unless component.controller.helpers.respond_to?(path_helper)
29
+
30
+ component.controller.helpers.public_send(path_helper, associated_record)
31
+ end
32
+
33
+ def verify_display_attribute
34
+ return if display_field.present?
35
+
36
+ raise ArgumentError,
37
+ "Must provide { attribute: :example_attribute } for `#{field}` or define " \
38
+ "`#{associated_record.class.name}.default_display_attribute`"
39
+ end
40
+
41
+ def display_value
42
+ associated_record.public_send(display_field)
43
+ end
44
+
45
+ def display_field
46
+ @display_field ||= options.fetch(:attribute) do
47
+ next associated_record.class.default_display_attribute if defined_display_attribute?
48
+ next default_display_attribute if default_display_attribute.present?
49
+
50
+ associated_record.class.primary_key
51
+ end
52
+ end
53
+
54
+ def defined_display_attribute?
55
+ associated_record.class.respond_to?(:default_display_attribute)
56
+ end
57
+
58
+ def namespace
59
+ component.controller.class.name.deconstantize.underscore
60
+ end
61
+
62
+ def resource_name
63
+ record.public_send(field).model_name.singular
64
+ end
65
+
66
+ def default_display_attribute
67
+ %i[name email display_name username].find do |display_field|
68
+ associated_record.respond_to?(display_field) && associated_record.method(display_field).arity.zero?
69
+ end
70
+ end
71
+
72
+ def path_helper
73
+ return "#{resource_name}_path" if namespace.blank?
74
+
75
+ "#{namespace}_#{resource_name}_path"
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Decorates a field by rendering a user-provided partial found in
7
+ # app/views/decorators/<model-name-plural>/_<field-name>.html.erb
8
+ class Decorator
9
+ def initialize(component:, item:, field:, value:)
10
+ @component = component
11
+ @item = item
12
+ @field = field
13
+ @value = value
14
+ end
15
+
16
+ def decorated_value
17
+ return default_decorated_value unless decorate?
18
+
19
+ render
20
+ end
21
+
22
+ private
23
+
24
+ attr_reader :component, :item, :field, :value
25
+
26
+ def render(sti: false)
27
+ component.controller.render_to_string(partial: decorator_path(sti: sti), locals: locals)
28
+ rescue ActionView::MissingTemplate
29
+ if sti
30
+ component.controller.missing_template_store[decorator_path] = true
31
+ default_decorated_value
32
+ else
33
+ render(sti: true)
34
+ end
35
+ end
36
+
37
+ def locals
38
+ {
39
+ record: item,
40
+ field: field,
41
+ default: value,
42
+ value: value, # Provide both default and value, default might change in future.
43
+ context: component.class.name.demodulize.underscore
44
+ }
45
+ end
46
+
47
+ def decorate?
48
+ return false unless item.class.is_a?(ActiveModel::Naming) || component.model_name.present?
49
+
50
+ !missing_template?
51
+ end
52
+
53
+ def missing_template?
54
+ component.controller.missing_template_store[decorator_path].present?
55
+ end
56
+
57
+ def decorator_path(sti: false)
58
+ Rails.root.join(
59
+ 'app/views/decorators',
60
+ model_path_name(sti: sti).pluralize,
61
+ field.to_s
62
+ ).relative_path_from(Rails.root.join('app/views')).to_s
63
+ end
64
+
65
+ def model_path_name(sti:)
66
+ return component.model_name if component.model_name.present?
67
+ return record_name(sti: sti) if record_name(sti: sti).present?
68
+ return item_record_name if item_record_name.present?
69
+
70
+ item.class.name
71
+ end
72
+
73
+ def record_name(sti:)
74
+ return Util.sti_record_name(item) if sti
75
+
76
+ Util.record_name(item).presence
77
+ end
78
+
79
+ def item_record_name
80
+ item.try(:model_name)&.singular
81
+ end
82
+
83
+ def render_with_default_decorator(template)
84
+ component.controller.render_to_string(
85
+ partial: "active_element/decorators/#{template}",
86
+ locals: { value: value }
87
+ )
88
+ end
89
+
90
+ def default_decorated_value # rubocop:disable Metrics/MethodLength
91
+ case value
92
+ when true, false
93
+ render_with_default_decorator('boolean')
94
+ when DateTime
95
+ render_with_default_decorator('datetime')
96
+ when Date
97
+ render_with_default_decorator('date')
98
+ when Time
99
+ render_with_default_decorator('time')
100
+ else
101
+ value
102
+ end
103
+ end
104
+ end
105
+ end
106
+ end
107
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveElement
4
+ module Components
5
+ module Util
6
+ # Maps ActiveRecord record fields to values for display (e.g. in tables).
7
+ class DisplayValueMapping
8
+ include RecordMapping
9
+
10
+ def numeric_value
11
+ value_from_record
12
+ end
13
+
14
+ def json_value
15
+ ActiveElement.json_pretty_print(value_from_record)
16
+ end
17
+
18
+ def string_value
19
+ value_from_record
20
+ end
21
+
22
+ def datetime_value
23
+ value_from_record.strftime('%Y-%m-%d %H:%M:%S')
24
+ end
25
+
26
+ def time_value
27
+ value_from_record.strftime('%H:%M:%S')
28
+ end
29
+
30
+ def date_value
31
+ value_from_record.strftime('%Y-%m-%d')
32
+ end
33
+
34
+ def boolean_value
35
+ component.controller.render_to_string(
36
+ partial: 'active_element/components/fields/boolean',
37
+ locals: { value: value_from_record }
38
+ )
39
+ end
40
+
41
+ def geometry_value
42
+ require 'rgeo/geo_json'
43
+ Util.json_pretty_print(RGeo::GeoJSON.encode(value_from_record))
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end