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.
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