active_element 0.0.1 → 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- 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
|