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.
- checksums.yaml +4 -4
- data/.rubocop.yml +12 -0
- data/.strong_versions.yml +2 -0
- data/Gemfile +10 -2
- data/Gemfile.lock +229 -4
- data/Rakefile +1 -0
- data/active_element.gemspec +7 -0
- data/app/assets/config/active_element/manifest.js +2 -0
- data/app/assets/javascripts/active_element/application.js +10 -0
- data/app/assets/javascripts/active_element/confirm.js +67 -0
- data/app/assets/javascripts/active_element/form.js +61 -0
- data/app/assets/javascripts/active_element/json_field.js +316 -0
- data/app/assets/javascripts/active_element/pagination.js +18 -0
- data/app/assets/javascripts/active_element/search_field.js +127 -0
- data/app/assets/javascripts/active_element/secret.js +40 -0
- data/app/assets/javascripts/active_element/setup.js +36 -0
- data/app/assets/javascripts/active_element/theme.js +42 -0
- data/app/assets/stylesheets/active_element/_variables.scss +142 -0
- data/app/assets/stylesheets/active_element/application.scss +77 -0
- data/app/controllers/active_element/application_controller.rb +41 -0
- data/app/controllers/active_element/text_searches_controller.rb +189 -0
- data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
- data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
- data/app/views/active_element/components/button.html.erb +27 -0
- data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
- data/app/views/active_element/components/form/_check_box.html.erb +3 -0
- data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
- data/app/views/active_element/components/form/_field.html.erb +28 -0
- data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
- data/app/views/active_element/components/form/_json.html.erb +12 -0
- data/app/views/active_element/components/form/_label.html.erb +17 -0
- data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
- data/app/views/active_element/components/form/_select.html.erb +4 -0
- data/app/views/active_element/components/form/_summary.html.erb +40 -0
- data/app/views/active_element/components/form/_templates.html.erb +85 -0
- data/app/views/active_element/components/form/_text_area.html.erb +4 -0
- data/app/views/active_element/components/form/_text_search.html.erb +16 -0
- data/app/views/active_element/components/form.html.erb +78 -0
- data/app/views/active_element/components/json.html.erb +8 -0
- data/app/views/active_element/components/page_description.html.erb +3 -0
- data/app/views/active_element/components/secret/_field.html.erb +1 -0
- data/app/views/active_element/components/secret/_templates.html.erb +11 -0
- data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
- data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
- data/app/views/active_element/components/table/_pagination.html.erb +17 -0
- data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
- data/app/views/active_element/components/table/collection.html.erb +39 -0
- data/app/views/active_element/components/table/item.html.erb +39 -0
- data/app/views/active_element/components/tabs.html.erb +7 -0
- data/app/views/active_element/decorators/_boolean.html.erb +5 -0
- data/app/views/active_element/decorators/_date.html.erb +3 -0
- data/app/views/active_element/decorators/_datetime.html.erb +3 -0
- data/app/views/active_element/decorators/_time.html.erb +3 -0
- data/app/views/active_element/forbidden.html.erb +33 -0
- data/app/views/active_element/main_menu/_item.html.erb +9 -0
- data/app/views/active_element/navbar/_menu.html.erb +30 -0
- data/app/views/active_element/theme/_select.html.erb +1 -0
- data/app/views/active_element/theme/_templates.html.erb +6 -0
- data/app/views/kaminari/_first_page.html.erb +3 -0
- data/app/views/kaminari/_gap.html.erb +3 -0
- data/app/views/kaminari/_last_page.html.erb +3 -0
- data/app/views/kaminari/_next_page.html.erb +3 -0
- data/app/views/kaminari/_page.html.erb +9 -0
- data/app/views/kaminari/_paginator.html.erb +17 -0
- data/app/views/kaminari/_prev_page.html.erb +3 -0
- data/app/views/layouts/active_element.html.erb +65 -0
- data/app/views/layouts/active_element_error.html.erb +40 -0
- data/config/routes.rb +5 -0
- data/lib/active_element/active_menu_link.rb +80 -0
- data/lib/active_element/active_record_text_search_authorization.rb +12 -0
- data/lib/active_element/colorized_string.rb +33 -0
- data/lib/active_element/component.rb +122 -0
- data/lib/active_element/components/button.rb +156 -0
- data/lib/active_element/components/collection_table.rb +118 -0
- data/lib/active_element/components/form.rb +210 -0
- data/lib/active_element/components/item_table.rb +57 -0
- data/lib/active_element/components/json.rb +31 -0
- data/lib/active_element/components/link_helpers.rb +9 -0
- data/lib/active_element/components/page_description.rb +28 -0
- data/lib/active_element/components/secret_fields.rb +15 -0
- data/lib/active_element/components/tab.rb +37 -0
- data/lib/active_element/components/tabs.rb +35 -0
- data/lib/active_element/components/translations.rb +18 -0
- data/lib/active_element/components/util/association_mapping.rb +80 -0
- data/lib/active_element/components/util/decorator.rb +107 -0
- data/lib/active_element/components/util/display_value_mapping.rb +48 -0
- data/lib/active_element/components/util/field_mapping.rb +144 -0
- data/lib/active_element/components/util/form_field_mapping.rb +104 -0
- data/lib/active_element/components/util/form_value_mapping.rb +49 -0
- data/lib/active_element/components/util/i18n.rb +66 -0
- data/lib/active_element/components/util/record_mapping.rb +111 -0
- data/lib/active_element/components/util.rb +43 -0
- data/lib/active_element/components.rb +20 -0
- data/lib/active_element/controller_action.rb +91 -0
- data/lib/active_element/engine.rb +26 -0
- data/lib/active_element/permissions_check.rb +101 -0
- data/lib/active_element/rails_component.rb +40 -0
- data/lib/active_element/route.rb +112 -0
- data/lib/active_element/routes.rb +62 -0
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +91 -1
- data/lib/tasks/active_element.rake +23 -0
- data/rspec-documentation/dummy +1 -0
- data/rspec-documentation/pages/Components/Forms.md +1 -0
- data/rspec-documentation/pages/Components/Tables.md +47 -0
- data/rspec-documentation/pages/Components/Tabs.md +1 -0
- data/rspec-documentation/pages/Components.md +1 -0
- data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
- data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
- data/rspec-documentation/pages/Index.md +3 -0
- data/rspec-documentation/pages/Util/I18n.md +1 -0
- data/rspec-documentation/spec_helper.rb +35 -0
- 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,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
|