active_element 0.0.2 → 0.0.4

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 (48) hide show
  1. checksums.yaml +4 -4
  2. data/.rubocop.yml +1 -0
  3. data/Gemfile +1 -1
  4. data/Gemfile.lock +8 -9
  5. data/active_element.gemspec +1 -1
  6. data/app/assets/javascripts/active_element/application.js +3 -1
  7. data/app/assets/javascripts/active_element/popover.js +6 -0
  8. data/app/assets/javascripts/active_element/setup.js +12 -0
  9. data/app/assets/javascripts/active_element/{search_field.js → text_search_field.js} +2 -2
  10. data/app/assets/javascripts/active_element/toast.js +8 -0
  11. data/app/assets/stylesheets/active_element/application.scss +65 -1
  12. data/app/controllers/active_element/application_controller.rb +13 -19
  13. data/app/views/active_element/components/form/_label.html.erb +2 -2
  14. data/app/views/active_element/components/form/_templates.html.erb +8 -8
  15. data/app/views/active_element/components/form.html.erb +3 -3
  16. data/app/views/active_element/components/table/_collection_row.html.erb +3 -3
  17. data/app/views/active_element/components/table/collection.html.erb +1 -1
  18. data/app/views/active_element/components/table/item.html.erb +3 -3
  19. data/app/views/active_element/navbar/_menu.html.erb +2 -2
  20. data/app/views/layouts/active_element.html.erb +21 -23
  21. data/app/views/layouts/active_element_error.html.erb +1 -1
  22. data/config/routes.rb +10 -1
  23. data/lib/active_element/components/button.rb +6 -41
  24. data/lib/active_element/components/form.rb +12 -3
  25. data/lib/active_element/components/text_search/active_record_authorization.rb +13 -0
  26. data/lib/active_element/components/text_search/authorization.rb +117 -0
  27. data/lib/active_element/components/text_search/component.rb +118 -0
  28. data/lib/active_element/components/text_search/sql.rb +107 -0
  29. data/lib/active_element/components/text_search.rb +23 -0
  30. data/lib/active_element/components/util/decorator.rb +2 -2
  31. data/lib/active_element/components/util/record_path.rb +84 -0
  32. data/lib/active_element/components/util.rb +7 -2
  33. data/lib/active_element/components.rb +1 -0
  34. data/lib/active_element/controller_action.rb +9 -10
  35. data/lib/active_element/controller_interface.rb +78 -0
  36. data/lib/active_element/permissions_check.rb +33 -29
  37. data/lib/active_element/permissions_report.rb +57 -0
  38. data/lib/active_element/route.rb +1 -1
  39. data/lib/active_element/routes.rb +1 -1
  40. data/lib/active_element/version.rb +1 -1
  41. data/lib/active_element.rb +24 -10
  42. data/lib/tasks/active_element.rake +1 -16
  43. data/rspec-documentation/pages/Components/Tables.md +2 -2
  44. data/rspec-documentation/spec_helper.rb +1 -1
  45. metadata +19 -12
  46. data/app/controllers/active_element/text_searches_controller.rb +0 -189
  47. data/lib/active_element/active_record_text_search_authorization.rb +0 -12
  48. data/lib/active_element/colorized_string.rb +0 -33
@@ -1,189 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Used by auto-complete search field for executing a text search on the provided model and
5
- # attributes.
6
- #
7
- # The user must have the permission `can_create_<application_name>_active_element_text_searches`.
8
- #
9
- # A model must call `authorize_active_element_text_search_for` to enable text search. e.g.:
10
- #
11
- # class MyModel < ApplicationRecord
12
- # authorize_active_element_text_search_for :name, exposes: [:email]
13
- # end
14
- #
15
- # This will allow searching on the `name` column and permits returning each matching record's
16
- # `:email` column's value.
17
- #
18
- # TODO: Refactor logic into separate classes.
19
- # rubocop:disable Metrics/ClassLength
20
- class TextSearchesController < ApplicationController
21
- DEFAULT_LIMIT = 50
22
-
23
- before_action :verify_parameters
24
- before_action :verify_model
25
-
26
- def create
27
- render json: { results: results, request_id: params[:request_id] }
28
- end
29
-
30
- private
31
-
32
- def verify_parameters
33
- return if %i[model attributes value query].all? { |parameter| params[parameter].present? }
34
-
35
- render json: { message: 'Must provide parameters: [model, attributes, value, query] for text search.' },
36
- status: :unprocessable_entity
37
- end
38
-
39
- def verify_model
40
- return if [model, search_columns, value_column].all?(&:present?) && authorized?
41
-
42
- render json: { message: verify_fail_message }, status: :unprocessable_entity
43
- end
44
-
45
- def verify_fail_message
46
- requested = %i[model attributes value].index_with { |key| params[key] }
47
- .compact_blank
48
- .map { |key, value| "#{key}: #{value}" }
49
- "Unpermitted or unavailable search for: { #{requested.join(', ')} }"
50
- end
51
-
52
- def results
53
- @results ||= model.where(*whereclause)
54
- .limit(limit)
55
- .pluck(value_column.name, *search_columns.map(&:name))
56
- .map { |value, *attributes| result(value, attributes) }
57
- end
58
-
59
- def result(value, attributes)
60
- { value: value, attributes: attributes.reject { |attribute| attribute == value } }
61
- end
62
-
63
- def model
64
- @model ||= params[:model].camelize(:upper).safe_constantize
65
- end
66
-
67
- def query
68
- params[:query]
69
- end
70
-
71
- def value_column
72
- return nil if params[:value].blank?
73
-
74
- @value_column ||= model&.columns&.find { |column| column.name == params[:value] }
75
- end
76
-
77
- def search_columns
78
- return nil if params[:attributes].blank?
79
-
80
- @search_columns ||= params[:attributes].map { |attribute| column_for(attribute) }.compact
81
- end
82
-
83
- def column_for(attribute)
84
- matched_column = model&.columns&.find { |column| column.name == attribute }
85
- return nil if matched_column.blank?
86
-
87
- compatible_column?(matched_column) ? matched_column : nil
88
- end
89
-
90
- def authorized?
91
- model.authorized_active_element_text_search_fields&.any? do |field, exposed|
92
- exposed = [exposed] unless exposed.is_a?(Array)
93
- authorized_field?(field, exposed)
94
- end
95
- end
96
-
97
- def authorized_field?(field, exposed)
98
- return false unless search_columns.map { |column| column.name.to_sym }.include?(field.to_sym)
99
- return false unless exposed&.map(&:to_sym)&.include?(value_column.name.to_sym)
100
-
101
- true
102
- end
103
-
104
- def whereclause
105
- clauses = search_columns.map { |column| "#{column.name} #{operator(column)} ?" }
106
- [clauses.join(' OR '), search_columns.map { |column| search_param(column) }].flatten
107
- end
108
-
109
- def operator(column)
110
- case column.type
111
- when :string
112
- model.connection.adapter_name == 'SQLite' ? 'LIKE' : 'ILIKE'
113
- else
114
- '='
115
- end
116
- end
117
-
118
- def compatible_column?(column) # rubocop:disable Metrics/MethodLength
119
- case column.type
120
- when :string
121
- true
122
- when :integer
123
- integer?
124
- when :float
125
- float?
126
- when :decimal
127
- decimal?
128
- else
129
- Rails.logger.info("Skipping query `#{query}` for incompatible column: #{column.name}")
130
- false
131
- end
132
- end
133
-
134
- def integer?
135
- Integer(query)
136
- true
137
- rescue ArgumentError
138
- false
139
- end
140
-
141
- def float?
142
- Float(query)
143
- true
144
- rescue ArgumentError
145
- false
146
- end
147
-
148
- def decimal?
149
- BigDecimal(query)
150
- true
151
- rescue ArgumentError
152
- false
153
- end
154
-
155
- def search_param(column)
156
- case column.type
157
- when :string
158
- "#{query}%"
159
- else
160
- query
161
- end
162
- end
163
-
164
- def limit
165
- DEFAULT_LIMIT
166
- end
167
-
168
- def permissions_check
169
- @permissions_check ||= PermissionsCheck.new(
170
- required: [],
171
- actual: current_user.permissions,
172
- controller_path: controller_path,
173
- action_name: action_name,
174
- rails_component: rails_component
175
- )
176
- end
177
-
178
- def required_permissions
179
- application_name = rails_component.application_name
180
- permission = "can_text_search_#{application_name}_#{params[:model]&.pluralize}"
181
- [[permission, { only: :create }]]
182
- end
183
-
184
- def rails_component
185
- @rails_component ||= RailsComponent.new(Rails)
186
- end
187
- end
188
- # rubocop:enable Metrics/ClassLength
189
- end
@@ -1,12 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- ActiveRecord::Base.class_eval do
4
- class << self
5
- attr_reader :authorized_active_element_text_search_fields
6
-
7
- def authorize_active_element_text_search_for(field, exposes:)
8
- @authorized_active_element_text_search_fields ||= []
9
- @authorized_active_element_text_search_fields << [field, exposes]
10
- end
11
- end
12
- end
@@ -1,33 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module ActiveElement
4
- # Wraps strings in terminal escape codes to provide colourisation in (e.g.) logging output.
5
- class ColorizedString
6
- COLOR_CODES = {
7
- cyan: '36',
8
- red: '31',
9
- green: '32',
10
- blue: '34',
11
- purple: '35',
12
- yellow: '33',
13
- light_gray: '37',
14
- light_blue: '1;34',
15
- white: '1;37'
16
- }.freeze
17
-
18
- def initialize(string, color:)
19
- @string = string
20
- @color = color
21
- end
22
-
23
- def value
24
- return string unless Rails.env.development? || Rails.env.test?
25
-
26
- "\e[#{COLOR_CODES.fetch(color)}m#{string}\e[0m"
27
- end
28
-
29
- private
30
-
31
- attr_reader :string, :color
32
- end
33
- end