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.
Files changed (113) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +12 -0
  3. data/.strong_versions.yml +2 -0
  4. data/Gemfile +10 -2
  5. data/Gemfile.lock +229 -4
  6. data/Rakefile +1 -0
  7. data/active_element.gemspec +7 -0
  8. data/app/assets/config/active_element/manifest.js +2 -0
  9. data/app/assets/javascripts/active_element/application.js +10 -0
  10. data/app/assets/javascripts/active_element/confirm.js +67 -0
  11. data/app/assets/javascripts/active_element/form.js +61 -0
  12. data/app/assets/javascripts/active_element/json_field.js +316 -0
  13. data/app/assets/javascripts/active_element/pagination.js +18 -0
  14. data/app/assets/javascripts/active_element/search_field.js +127 -0
  15. data/app/assets/javascripts/active_element/secret.js +40 -0
  16. data/app/assets/javascripts/active_element/setup.js +36 -0
  17. data/app/assets/javascripts/active_element/theme.js +42 -0
  18. data/app/assets/stylesheets/active_element/_variables.scss +142 -0
  19. data/app/assets/stylesheets/active_element/application.scss +77 -0
  20. data/app/controllers/active_element/application_controller.rb +41 -0
  21. data/app/controllers/active_element/text_searches_controller.rb +189 -0
  22. data/app/views/active_element/components/_horizontal_tabs.html.erb +32 -0
  23. data/app/views/active_element/components/_vertical_tabs.html.erb +38 -0
  24. data/app/views/active_element/components/button.html.erb +27 -0
  25. data/app/views/active_element/components/fields/_boolean.html.erb +11 -0
  26. data/app/views/active_element/components/form/_check_box.html.erb +3 -0
  27. data/app/views/active_element/components/form/_check_boxes.html.erb +33 -0
  28. data/app/views/active_element/components/form/_field.html.erb +28 -0
  29. data/app/views/active_element/components/form/_generic_field.html.erb +3 -0
  30. data/app/views/active_element/components/form/_json.html.erb +12 -0
  31. data/app/views/active_element/components/form/_label.html.erb +17 -0
  32. data/app/views/active_element/components/form/_option_groups_summary.html.erb +17 -0
  33. data/app/views/active_element/components/form/_select.html.erb +4 -0
  34. data/app/views/active_element/components/form/_summary.html.erb +40 -0
  35. data/app/views/active_element/components/form/_templates.html.erb +85 -0
  36. data/app/views/active_element/components/form/_text_area.html.erb +4 -0
  37. data/app/views/active_element/components/form/_text_search.html.erb +16 -0
  38. data/app/views/active_element/components/form.html.erb +78 -0
  39. data/app/views/active_element/components/json.html.erb +8 -0
  40. data/app/views/active_element/components/page_description.html.erb +3 -0
  41. data/app/views/active_element/components/secret/_field.html.erb +1 -0
  42. data/app/views/active_element/components/secret/_templates.html.erb +11 -0
  43. data/app/views/active_element/components/table/_collection_row.html.erb +30 -0
  44. data/app/views/active_element/components/table/_grouped_collection.html.erb +88 -0
  45. data/app/views/active_element/components/table/_pagination.html.erb +17 -0
  46. data/app/views/active_element/components/table/_ungrouped_collection.html.erb +49 -0
  47. data/app/views/active_element/components/table/collection.html.erb +39 -0
  48. data/app/views/active_element/components/table/item.html.erb +39 -0
  49. data/app/views/active_element/components/tabs.html.erb +7 -0
  50. data/app/views/active_element/decorators/_boolean.html.erb +5 -0
  51. data/app/views/active_element/decorators/_date.html.erb +3 -0
  52. data/app/views/active_element/decorators/_datetime.html.erb +3 -0
  53. data/app/views/active_element/decorators/_time.html.erb +3 -0
  54. data/app/views/active_element/forbidden.html.erb +33 -0
  55. data/app/views/active_element/main_menu/_item.html.erb +9 -0
  56. data/app/views/active_element/navbar/_menu.html.erb +30 -0
  57. data/app/views/active_element/theme/_select.html.erb +1 -0
  58. data/app/views/active_element/theme/_templates.html.erb +6 -0
  59. data/app/views/kaminari/_first_page.html.erb +3 -0
  60. data/app/views/kaminari/_gap.html.erb +3 -0
  61. data/app/views/kaminari/_last_page.html.erb +3 -0
  62. data/app/views/kaminari/_next_page.html.erb +3 -0
  63. data/app/views/kaminari/_page.html.erb +9 -0
  64. data/app/views/kaminari/_paginator.html.erb +17 -0
  65. data/app/views/kaminari/_prev_page.html.erb +3 -0
  66. data/app/views/layouts/active_element.html.erb +65 -0
  67. data/app/views/layouts/active_element_error.html.erb +40 -0
  68. data/config/routes.rb +5 -0
  69. data/lib/active_element/active_menu_link.rb +80 -0
  70. data/lib/active_element/active_record_text_search_authorization.rb +12 -0
  71. data/lib/active_element/colorized_string.rb +33 -0
  72. data/lib/active_element/component.rb +122 -0
  73. data/lib/active_element/components/button.rb +156 -0
  74. data/lib/active_element/components/collection_table.rb +118 -0
  75. data/lib/active_element/components/form.rb +210 -0
  76. data/lib/active_element/components/item_table.rb +57 -0
  77. data/lib/active_element/components/json.rb +31 -0
  78. data/lib/active_element/components/link_helpers.rb +9 -0
  79. data/lib/active_element/components/page_description.rb +28 -0
  80. data/lib/active_element/components/secret_fields.rb +15 -0
  81. data/lib/active_element/components/tab.rb +37 -0
  82. data/lib/active_element/components/tabs.rb +35 -0
  83. data/lib/active_element/components/translations.rb +18 -0
  84. data/lib/active_element/components/util/association_mapping.rb +80 -0
  85. data/lib/active_element/components/util/decorator.rb +107 -0
  86. data/lib/active_element/components/util/display_value_mapping.rb +48 -0
  87. data/lib/active_element/components/util/field_mapping.rb +144 -0
  88. data/lib/active_element/components/util/form_field_mapping.rb +104 -0
  89. data/lib/active_element/components/util/form_value_mapping.rb +49 -0
  90. data/lib/active_element/components/util/i18n.rb +66 -0
  91. data/lib/active_element/components/util/record_mapping.rb +111 -0
  92. data/lib/active_element/components/util.rb +43 -0
  93. data/lib/active_element/components.rb +20 -0
  94. data/lib/active_element/controller_action.rb +91 -0
  95. data/lib/active_element/engine.rb +26 -0
  96. data/lib/active_element/permissions_check.rb +101 -0
  97. data/lib/active_element/rails_component.rb +40 -0
  98. data/lib/active_element/route.rb +112 -0
  99. data/lib/active_element/routes.rb +62 -0
  100. data/lib/active_element/version.rb +1 -1
  101. data/lib/active_element.rb +91 -1
  102. data/lib/tasks/active_element.rake +23 -0
  103. data/rspec-documentation/dummy +1 -0
  104. data/rspec-documentation/pages/Components/Forms.md +1 -0
  105. data/rspec-documentation/pages/Components/Tables.md +47 -0
  106. data/rspec-documentation/pages/Components/Tabs.md +1 -0
  107. data/rspec-documentation/pages/Components.md +1 -0
  108. data/rspec-documentation/pages/Decorators/Inline Decorators.md +1 -0
  109. data/rspec-documentation/pages/Decorators/View Decorators.md +1 -0
  110. data/rspec-documentation/pages/Index.md +3 -0
  111. data/rspec-documentation/pages/Util/I18n.md +1 -0
  112. data/rspec-documentation/spec_helper.rb +35 -0
  113. metadata +191 -3
@@ -0,0 +1,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