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,144 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
module Components
|
|
5
|
+
module Util
|
|
6
|
+
# Utility class for converting field names to labels, CSS classes, and data mappers.
|
|
7
|
+
class FieldMapping # rubocop:disable Metrics/ClassLength
|
|
8
|
+
include Translations
|
|
9
|
+
|
|
10
|
+
def initialize(component, fields, class_name)
|
|
11
|
+
@component = component
|
|
12
|
+
@class_name = class_name
|
|
13
|
+
@fields = fields
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def mapped_fields
|
|
17
|
+
fields.map do |field|
|
|
18
|
+
[
|
|
19
|
+
field,
|
|
20
|
+
class_mapper(field),
|
|
21
|
+
field_to_label(field),
|
|
22
|
+
decorated_value_mapper(field),
|
|
23
|
+
options(field)
|
|
24
|
+
]
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
delegate :model, to: :component
|
|
29
|
+
|
|
30
|
+
private
|
|
31
|
+
|
|
32
|
+
attr_reader :component, :fields, :class_name
|
|
33
|
+
|
|
34
|
+
def options(field)
|
|
35
|
+
{ description: i18n.description(field) }
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def decorated_value_mapper(field)
|
|
39
|
+
proc do |item|
|
|
40
|
+
Decorator.new(
|
|
41
|
+
component: component,
|
|
42
|
+
item: item,
|
|
43
|
+
field: field,
|
|
44
|
+
value: value_mapper(field).call(item)
|
|
45
|
+
).decorated_value
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def field_to_label(field)
|
|
50
|
+
return 'ID' if field == :id # Move to i18n if this gets more complex.
|
|
51
|
+
return i18n.label(field.first) if field.is_a?(Array)
|
|
52
|
+
|
|
53
|
+
i18n.label(field)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def value_mapper(field)
|
|
57
|
+
case field
|
|
58
|
+
when String, Symbol
|
|
59
|
+
default_value_mapper(field)
|
|
60
|
+
when Array
|
|
61
|
+
field.last.fetch(:mapper) { default_value_mapper(*field) }
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def class_mapper(field)
|
|
66
|
+
proc do |item|
|
|
67
|
+
next default_record_classes(field, item).compact.join(' ') if item.class.is_a?(ActiveModel::Naming)
|
|
68
|
+
|
|
69
|
+
field_class(field)
|
|
70
|
+
end
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def default_value_mapper(field, options = nil)
|
|
74
|
+
proc do |item|
|
|
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)
|
|
78
|
+
|
|
79
|
+
nil
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def hash_field?(item, field)
|
|
84
|
+
item.respond_to?(:[]) && item.respond_to?(:key?) && item.key?(field)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def default_record_value(field, record, options)
|
|
88
|
+
Util::DisplayValueMapping.new(
|
|
89
|
+
component: component,
|
|
90
|
+
field: field,
|
|
91
|
+
record: record,
|
|
92
|
+
options: options
|
|
93
|
+
).value
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_record_classes(field, record)
|
|
97
|
+
if field.is_a?(Array)
|
|
98
|
+
return [
|
|
99
|
+
inferred_class(field.first, record, field.last),
|
|
100
|
+
field_class(field, field.last),
|
|
101
|
+
field.last[:class]
|
|
102
|
+
]
|
|
103
|
+
end
|
|
104
|
+
|
|
105
|
+
[inferred_class(field, record), field_class(field)]
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def inferred_class(field, record, options = nil)
|
|
109
|
+
{
|
|
110
|
+
integer: 'font-monospace', decimal: 'font-monospace', float: 'font-monospace',
|
|
111
|
+
datetime: 'font-monospace', date: 'font-monospace', time: 'font-monospace'
|
|
112
|
+
}.fetch(
|
|
113
|
+
Util::DisplayValueMapping.new(
|
|
114
|
+
component: component, field: field, record: record, options: options
|
|
115
|
+
).type,
|
|
116
|
+
nil
|
|
117
|
+
)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def field_to_name(field)
|
|
121
|
+
return field.to_s if field.is_a?(Symbol) || field.is_a?(String)
|
|
122
|
+
return Util::I18n.class_name(field.first) if field.is_a?(Array)
|
|
123
|
+
|
|
124
|
+
nil
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
def field_class(field, options = {})
|
|
128
|
+
field_name = field_to_name(field)
|
|
129
|
+
base = class_name.blank? ? field_name : "#{class_name}-#{field_name}"
|
|
130
|
+
[base, format_classes(field_name, options.fetch(:format, nil))].flatten.compact.join(' ')
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def format_classes(field_name, format_from_options)
|
|
134
|
+
format_from_translation = i18n.format(field_name)
|
|
135
|
+
[format_class(format_from_translation), format_class(format_from_options)].compact
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
def format_class(format)
|
|
139
|
+
{ bold: 'fw-bold', monospace: 'font-monospace' }.fetch(format&.to_sym, nil)
|
|
140
|
+
end
|
|
141
|
+
end
|
|
142
|
+
end
|
|
143
|
+
end
|
|
144
|
+
end
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
module Components
|
|
5
|
+
module Util
|
|
6
|
+
# Normalizes Form `fields` parameter from various supported input formats.
|
|
7
|
+
class FormFieldMapping
|
|
8
|
+
include SecretFields
|
|
9
|
+
|
|
10
|
+
def initialize(record, fields, i18n)
|
|
11
|
+
@record = record
|
|
12
|
+
@fields = fields
|
|
13
|
+
@i18n = i18n
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def fields_with_types_and_options
|
|
17
|
+
compiled_fields = fields.map do |field|
|
|
18
|
+
next field_with_default_type_and_default_options(field) unless field.is_a?(Array)
|
|
19
|
+
next field if normalized_field?(field)
|
|
20
|
+
next field_with_default_type_and_provided_options(field) if field_name_with_options?(field)
|
|
21
|
+
next field_with_type(field) if field_name_with_type?(field)
|
|
22
|
+
|
|
23
|
+
raise_unrecognized_field_format(field)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
fields_with_default_label(compiled_fields)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :fields, :i18n, :record
|
|
32
|
+
|
|
33
|
+
def normalized_field?(field)
|
|
34
|
+
(field.size == 3) && field.last.is_a?(Hash)
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def field_name_with_options?(field)
|
|
38
|
+
field.size == 2 && field.last.is_a?(Hash)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def field_name_with_type?(field)
|
|
42
|
+
field.size == 2
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def field_with_default_type_and_default_options(field)
|
|
46
|
+
[field, default_type_from_model(field), {}]
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def field_with_type(field)
|
|
50
|
+
[field.first, field.last, {}]
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
def default_type_from_model(field)
|
|
54
|
+
return default_field_type(field) if record.blank?
|
|
55
|
+
|
|
56
|
+
column = record.class.columns.find { |model_column| model_column.name.to_s == field.to_s }
|
|
57
|
+
return default_field_type(field) if column.blank?
|
|
58
|
+
|
|
59
|
+
default_type_from_column_type(field, column.type)
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def default_type_from_column_type(field, column_type)
|
|
63
|
+
{
|
|
64
|
+
string: default_field_type(field),
|
|
65
|
+
boolean: :check_box,
|
|
66
|
+
json: :json_field,
|
|
67
|
+
jsonb: :json_field,
|
|
68
|
+
geometry: :text_area
|
|
69
|
+
}.fetch(column_type.to_sym, default_field_type(field))
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
def field_with_default_type_and_provided_options(field)
|
|
73
|
+
[field.first, default_field_type(field), field.last]
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def fields_with_default_label(fields)
|
|
77
|
+
fields.map do |field, type, options|
|
|
78
|
+
[field, type, options_with_inferred_translations(field, options)]
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
def raise_unrecognized_field_format(field)
|
|
83
|
+
raise ArgumentError, "Unexpected field format: #{field}, expected one of: " \
|
|
84
|
+
':field_name, [:field_name, :text_field], ' \
|
|
85
|
+
"or [:field_name, :text_field, { label: 'Field Name', ... }"
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def options_with_inferred_translations(field, options)
|
|
89
|
+
options.reverse_merge({
|
|
90
|
+
label: i18n.label(field),
|
|
91
|
+
description: i18n.description(field),
|
|
92
|
+
placeholder: i18n.placeholder(field)
|
|
93
|
+
})
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def default_field_type(field)
|
|
97
|
+
return :password_field if secret_field?(field)
|
|
98
|
+
|
|
99
|
+
:text_field
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
104
|
+
end
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
module Components
|
|
5
|
+
module Util
|
|
6
|
+
# Maps ActiveRecord record fields to values for editing in forms.
|
|
7
|
+
class FormValueMapping
|
|
8
|
+
include RecordMapping
|
|
9
|
+
|
|
10
|
+
def numeric_value
|
|
11
|
+
value_from_record
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def json_value
|
|
15
|
+
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
|
+
value_from_record
|
|
36
|
+
component.controller.render_to_string(
|
|
37
|
+
partial: 'active_element/components/fields/boolean',
|
|
38
|
+
locals: { value: value_from_record }
|
|
39
|
+
)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def geometry_value
|
|
43
|
+
require 'rgeo/geo_json'
|
|
44
|
+
RGeo::GeoJSON.encode(value_from_record).to_json
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
module Components
|
|
5
|
+
module Util
|
|
6
|
+
# Translates various parameters using `I18n` library, allows users to specify labels,
|
|
7
|
+
# descriptions, placeholders, etc. for various components in locales files.
|
|
8
|
+
class I18n
|
|
9
|
+
def self.class_name(val, plural: false)
|
|
10
|
+
base = val&.to_s&.underscore&.tr('_', '-')&.tr('/', '-')
|
|
11
|
+
plural ? base&.pluralize : base
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def initialize(component)
|
|
15
|
+
@component = component
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def label(field)
|
|
19
|
+
return titleize(field) unless model?
|
|
20
|
+
|
|
21
|
+
key = "admin.models.#{model_key}.fields.#{field}.label"
|
|
22
|
+
::I18n.t(key, default: titleize(field))
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def description(field)
|
|
26
|
+
return nil unless model?
|
|
27
|
+
|
|
28
|
+
key = "admin.models.#{model_key}.fields.#{field}.description"
|
|
29
|
+
::I18n.t(key, default: nil)
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def placeholder(field)
|
|
33
|
+
return nil unless model?
|
|
34
|
+
|
|
35
|
+
key = "admin.models.#{model_key}.fields.#{field}.placeholder"
|
|
36
|
+
::I18n.t(key, default: nil)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format(field)
|
|
40
|
+
return nil unless model?
|
|
41
|
+
|
|
42
|
+
key = "admin.models.#{model_key}.fields.#{field}.format"
|
|
43
|
+
::I18n.t(key, default: nil)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
private
|
|
47
|
+
|
|
48
|
+
attr_reader :component
|
|
49
|
+
|
|
50
|
+
def model_key
|
|
51
|
+
@model_key ||= component.model.name.underscore.pluralize
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def model?
|
|
55
|
+
return false if component.model.nil?
|
|
56
|
+
|
|
57
|
+
component.model.is_a?(ActiveModel::Naming)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def titleize(field)
|
|
61
|
+
field.to_s.titleize(keep_id_suffix: true).gsub(/ Id$/, ' ID')
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
module Components
|
|
5
|
+
module Util
|
|
6
|
+
# Maps ActiveRecord record columns and fields to values.
|
|
7
|
+
module RecordMapping
|
|
8
|
+
DATABASE_TYPES = %i[
|
|
9
|
+
string json jsonb integer decimal float datetime time date boolean binary geometry
|
|
10
|
+
].freeze
|
|
11
|
+
|
|
12
|
+
def initialize(component:, record:, field:, options: {})
|
|
13
|
+
@component = component
|
|
14
|
+
@record = record
|
|
15
|
+
@field = field
|
|
16
|
+
@options = options
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def value
|
|
20
|
+
return mapped_value_from_record if association? || column.present?
|
|
21
|
+
|
|
22
|
+
value_from_record
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def type
|
|
26
|
+
column&.type
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
private
|
|
30
|
+
|
|
31
|
+
attr_reader :component, :record, :field, :options
|
|
32
|
+
|
|
33
|
+
def column
|
|
34
|
+
return nil unless record.is_a?(ActiveRecord::Base)
|
|
35
|
+
|
|
36
|
+
@column ||= record.class.columns.find { |model_column| model_column.name.to_s == field.to_s }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def association?
|
|
40
|
+
return false unless record.is_a?(ActiveRecord::Base)
|
|
41
|
+
|
|
42
|
+
record.association(field).present?
|
|
43
|
+
rescue ActiveRecord::AssociationNotFoundError
|
|
44
|
+
false
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def value_from_record
|
|
48
|
+
record.public_send(field) if record.respond_to?(field)
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def mapped_value_from_record
|
|
52
|
+
return mapped_association_from_record if value_from_record.class.is_a?(ActiveModel::Naming)
|
|
53
|
+
return nil if value_from_record.nil?
|
|
54
|
+
return value_from_record unless DATABASE_TYPES.include?(column.type) || value_from_record.blank?
|
|
55
|
+
|
|
56
|
+
send("#{column.type}_value")
|
|
57
|
+
end
|
|
58
|
+
|
|
59
|
+
def mapped_association_from_record
|
|
60
|
+
AssociationMapping.new(
|
|
61
|
+
component: component,
|
|
62
|
+
field: field,
|
|
63
|
+
record: record,
|
|
64
|
+
associated_record: value_from_record,
|
|
65
|
+
options: options
|
|
66
|
+
).link_tag
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
# Override these methods as required in a class that includes this module:
|
|
70
|
+
|
|
71
|
+
def numeric_value
|
|
72
|
+
value_from_record
|
|
73
|
+
end
|
|
74
|
+
alias integer_value numeric_value
|
|
75
|
+
alias decimal_value numeric_value
|
|
76
|
+
alias float_value numeric_value
|
|
77
|
+
|
|
78
|
+
def json_value
|
|
79
|
+
value_from_record
|
|
80
|
+
end
|
|
81
|
+
alias jsonb_value json_value
|
|
82
|
+
|
|
83
|
+
def string_value
|
|
84
|
+
value_from_record
|
|
85
|
+
end
|
|
86
|
+
alias text_value string_value
|
|
87
|
+
|
|
88
|
+
def datetime_value
|
|
89
|
+
value_from_record
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
def time_value
|
|
93
|
+
value_from_record
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
def date_value
|
|
97
|
+
value_from_record
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
def boolean_value
|
|
101
|
+
value_from_record
|
|
102
|
+
end
|
|
103
|
+
alias binary_value boolean_value
|
|
104
|
+
|
|
105
|
+
def geometry_value
|
|
106
|
+
value_from_record
|
|
107
|
+
end
|
|
108
|
+
end
|
|
109
|
+
end
|
|
110
|
+
end
|
|
111
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'util/i18n'
|
|
4
|
+
require_relative 'util/record_mapping'
|
|
5
|
+
require_relative 'util/field_mapping'
|
|
6
|
+
require_relative 'util/form_field_mapping'
|
|
7
|
+
require_relative 'util/form_value_mapping'
|
|
8
|
+
require_relative 'util/display_value_mapping'
|
|
9
|
+
require_relative 'util/association_mapping'
|
|
10
|
+
require_relative 'util/decorator'
|
|
11
|
+
|
|
12
|
+
module ActiveElement
|
|
13
|
+
module Components
|
|
14
|
+
# Utility classes for components (data mapping from models, etc.)
|
|
15
|
+
module Util
|
|
16
|
+
def self.json_name(name)
|
|
17
|
+
name.to_s.camelize(:lower)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def self.record_name(record)
|
|
21
|
+
record&.try(:model_name)&.try(&:singular) || record.class.name.demodulize.underscore
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def self.sti_record_name(record)
|
|
25
|
+
return nil unless record.class.respond_to?(:inheritance_column)
|
|
26
|
+
|
|
27
|
+
record&.class&.superclass&.model_name&.singular if record&.try(record.class.inheritance_column).present?
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def self.json_pretty_print(json)
|
|
31
|
+
theme = Rouge::Themes::Base16.mode(:light)
|
|
32
|
+
formatter = Rouge::Formatters::HTMLLinewise.new(Rouge::Formatters::HTMLInline.new(theme))
|
|
33
|
+
lexer = Rouge::Lexers::JSON.new
|
|
34
|
+
content = JSON.pretty_generate(json.is_a?(String) ? JSON.parse(json) : json)
|
|
35
|
+
formatted = formatter.format(lexer.lex(content)).gsub(' ', ' ')
|
|
36
|
+
# rubocop:disable Rails/OutputSafety
|
|
37
|
+
# TODO: Move to a template.
|
|
38
|
+
"<div style='font-family: monospace;'>#{formatted}</div>".html_safe
|
|
39
|
+
# rubocop:enable Rails/OutputSafety
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'components/translations'
|
|
4
|
+
require_relative 'components/secret_fields'
|
|
5
|
+
require_relative 'components/util'
|
|
6
|
+
require_relative 'components/link_helpers'
|
|
7
|
+
require_relative 'components/page_description'
|
|
8
|
+
require_relative 'components/form'
|
|
9
|
+
require_relative 'components/collection_table'
|
|
10
|
+
require_relative 'components/item_table'
|
|
11
|
+
require_relative 'components/button'
|
|
12
|
+
require_relative 'components/tabs'
|
|
13
|
+
require_relative 'components/tab'
|
|
14
|
+
require_relative 'components/json'
|
|
15
|
+
|
|
16
|
+
module ActiveElement
|
|
17
|
+
# Standardised HTML components to be used in admin front ends.
|
|
18
|
+
module Components
|
|
19
|
+
end
|
|
20
|
+
end
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
# Processes all controller actions, verifies permissions, issues redirects, etc.
|
|
5
|
+
class ControllerAction
|
|
6
|
+
def initialize(controller)
|
|
7
|
+
@controller = controller
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def process_action
|
|
11
|
+
Rails.logger.info("[#{log_tag}] #{colorized_permissions_message}")
|
|
12
|
+
return if verified_permissions?
|
|
13
|
+
|
|
14
|
+
warn "[#{log_tag}] #{colorized_permissions_message}" if Rails.env.test?
|
|
15
|
+
return controller.redirect_to redirect_path if redirect_to_default_landing_page?
|
|
16
|
+
|
|
17
|
+
render_forbidden
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
private
|
|
21
|
+
|
|
22
|
+
attr_reader :controller
|
|
23
|
+
|
|
24
|
+
def verified_permissions?
|
|
25
|
+
return @verified_permissions if defined?(@verified_permissions)
|
|
26
|
+
|
|
27
|
+
(@verified_permissions = permissions_check.permitted?)
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def redirect_path
|
|
31
|
+
routes.alternative_routes.first.path
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def render_forbidden
|
|
35
|
+
return render_json_forbidden if controller.request.format == :json
|
|
36
|
+
|
|
37
|
+
controller.render 'active_element/forbidden',
|
|
38
|
+
layout: 'active_element_error',
|
|
39
|
+
status: :forbidden,
|
|
40
|
+
locals: {
|
|
41
|
+
missing_permissions: permissions_check.missing,
|
|
42
|
+
alternatives: routes.alternative_routes
|
|
43
|
+
}
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def render_json_forbidden
|
|
47
|
+
controller.render json: { message: "Missing permission(s): #{permissions_check.missing}" },
|
|
48
|
+
status: :forbidden
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def permissions_check
|
|
52
|
+
@permissions_check ||= PermissionsCheck.new(
|
|
53
|
+
required: controller.class.active_element_permissions,
|
|
54
|
+
actual: controller.current_user&.permissions,
|
|
55
|
+
controller_path: controller.controller_path,
|
|
56
|
+
action_name: controller.action_name,
|
|
57
|
+
rails_component: rails_component
|
|
58
|
+
)
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def routes
|
|
62
|
+
@routes ||= Routes.new(
|
|
63
|
+
permissions: controller.current_user.permissions,
|
|
64
|
+
rails_component: rails_component
|
|
65
|
+
)
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
def colorized_permissions_message
|
|
69
|
+
color = if permissions_check.permitted?
|
|
70
|
+
:green
|
|
71
|
+
else
|
|
72
|
+
(rails_component.environment == 'test' ? :yellow : :red)
|
|
73
|
+
end
|
|
74
|
+
ColorizedString.new(permissions_check.message, color: color).value
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def redirect_to_default_landing_page?
|
|
78
|
+
return false if controller.request.format == :json
|
|
79
|
+
|
|
80
|
+
controller.request.path == '/' && routes.alternative_routes.present?
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def rails_component
|
|
84
|
+
@rails_component ||= RailsComponent.new(::Rails)
|
|
85
|
+
end
|
|
86
|
+
|
|
87
|
+
def log_tag
|
|
88
|
+
ColorizedString.new('ActiveElement', color: :cyan).value
|
|
89
|
+
end
|
|
90
|
+
end
|
|
91
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module ActiveElement
|
|
4
|
+
# Provides initial setup and gem integration for host Rails application.
|
|
5
|
+
class Engine < ::Rails::Engine
|
|
6
|
+
initializer 'active_element.precompile' do |app|
|
|
7
|
+
next unless app.config.respond_to?(:assets)
|
|
8
|
+
|
|
9
|
+
app.config.assets.precompile += %w[
|
|
10
|
+
active_element/manifest.js
|
|
11
|
+
]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
initializer 'active_element.routes' do |app|
|
|
15
|
+
app.routes.append do
|
|
16
|
+
mount Engine => '/'
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
initializer 'active_element.silence_action_view_notifications', after: 'finisher_hook' do
|
|
21
|
+
next unless ActiveElement.silence_logging?
|
|
22
|
+
|
|
23
|
+
warn '*** Rails ActionView logging events are disabled by default. Set ACTIVE_ELEMENT_DEBUG=1 to enable.'
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|