active_element 0.0.1 → 0.0.2

Sign up to get free protection for your applications and to get access to all the features.
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 +11 -2
  5. data/Gemfile.lock +230 -3
  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