active_element 0.0.11 → 0.0.13
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 +1 -1
- data/Gemfile +2 -1
- data/Gemfile.lock +10 -3
- data/app/assets/javascripts/active_element/highlight.js +311 -0
- data/app/assets/javascripts/active_element/json_field.js +51 -20
- data/app/assets/javascripts/active_element/popover.js +6 -4
- data/app/assets/javascripts/active_element/text_search_field.js +13 -1
- data/app/assets/stylesheets/active_element/_dark.scss +1 -1
- data/app/assets/stylesheets/active_element/application.scss +39 -1
- data/app/controllers/concerns/active_element/default_controller_actions.rb +7 -7
- data/app/views/active_element/components/fields/_json.html.erb +3 -2
- data/app/views/active_element/components/form/_field.html.erb +2 -1
- data/app/views/active_element/components/form/_json.html.erb +2 -0
- data/app/views/active_element/components/form/_label.html.erb +8 -1
- data/app/views/active_element/components/form/_templates.html.erb +8 -5
- data/app/views/active_element/components/form/_text_search.html.erb +1 -1
- data/app/views/active_element/components/form.html.erb +2 -2
- data/app/views/active_element/components/table/_field.html.erb +1 -1
- data/app/views/active_element/components/table/_ungrouped_collection.html.erb +1 -0
- data/app/views/active_element/components/table/item.html.erb +1 -0
- data/app/views/active_element/default_views/edit.html.erb +2 -2
- data/app/views/active_element/default_views/forbidden.html.erb +7 -0
- data/app/views/active_element/default_views/index.html.erb +7 -7
- data/app/views/active_element/default_views/new.html.erb +1 -1
- data/app/views/active_element/default_views/show.html.erb +3 -3
- data/app/views/layouts/active_element.html.erb +25 -4
- data/config/locales/en.yml +3 -0
- data/example_app/Gemfile.lock +1 -1
- data/example_app/app/controllers/pets_controller.rb +1 -0
- data/example_app/app/controllers/users_controller.rb +1 -0
- data/lib/active_element/components/form.rb +1 -8
- data/lib/active_element/components/text_search.rb +10 -1
- data/lib/active_element/components/util/display_value_mapping.rb +0 -2
- data/lib/active_element/components/util/form_field_mapping.rb +23 -10
- data/lib/active_element/components/util/numeric_field.rb +73 -0
- data/lib/active_element/components/util.rb +8 -0
- data/lib/active_element/controller_interface.rb +27 -30
- data/lib/active_element/controller_state.rb +44 -0
- data/lib/active_element/default_controller/actions.rb +3 -0
- data/lib/active_element/default_controller/controller.rb +145 -0
- data/lib/active_element/default_controller/json_params.rb +48 -0
- data/lib/active_element/default_controller/params.rb +97 -0
- data/lib/active_element/default_controller/search.rb +112 -0
- data/lib/active_element/default_controller.rb +10 -88
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +1 -2
- data/rspec-documentation/_head.html.erb +2 -0
- data/rspec-documentation/pages/000-Introduction.md +8 -5
- data/rspec-documentation/pages/005-Setup.md +21 -28
- data/rspec-documentation/pages/010-Components/Form Fields.md +35 -0
- data/rspec-documentation/pages/015-Custom Controllers.md +32 -0
- data/rspec-documentation/pages/016-Default Controller.md +132 -0
- data/rspec-documentation/pages/Themes.md +3 -0
- metadata +15 -4
- data/lib/active_element/default_record_params.rb +0 -62
- data/lib/active_element/default_text_search.rb +0 -110
@@ -16,7 +16,7 @@ module ActiveElement
|
|
16
16
|
def initialize(controller_class, controller_instance = nil)
|
17
17
|
@controller_class = controller_class
|
18
18
|
@controller_instance = controller_instance
|
19
|
-
initialize_state
|
19
|
+
initialize_state(controller_class)
|
20
20
|
@missing_template_store = {}
|
21
21
|
@authorize = false
|
22
22
|
end
|
@@ -25,20 +25,25 @@ module ActiveElement
|
|
25
25
|
@authorize
|
26
26
|
end
|
27
27
|
|
28
|
-
def listable_fields(*args)
|
29
|
-
state
|
28
|
+
def listable_fields(*args, order: nil)
|
29
|
+
state.list_order = order
|
30
|
+
state.listable_fields.concat(args.map(&:to_sym)).uniq!
|
30
31
|
end
|
31
32
|
|
32
33
|
def viewable_fields(*args)
|
33
|
-
state
|
34
|
+
state.viewable_fields.concat(args.map(&:to_sym)).uniq!
|
34
35
|
end
|
35
36
|
|
36
37
|
def editable_fields(*args)
|
37
|
-
state
|
38
|
+
state.editable_fields.concat(args.map(&:to_sym)).uniq!
|
38
39
|
end
|
39
40
|
|
40
41
|
def searchable_fields(*args)
|
41
|
-
state
|
42
|
+
state.searchable_fields.concat(args.map(&:to_sym)).uniq!
|
43
|
+
end
|
44
|
+
|
45
|
+
def deletable
|
46
|
+
state.deletable = true
|
42
47
|
end
|
43
48
|
|
44
49
|
def application_name
|
@@ -46,43 +51,39 @@ module ActiveElement
|
|
46
51
|
end
|
47
52
|
|
48
53
|
def authenticate_with(&block)
|
49
|
-
state
|
54
|
+
state.authenticator = block
|
50
55
|
end
|
51
56
|
|
52
57
|
def authorize_with(&block)
|
53
58
|
@authorize = true
|
54
|
-
state
|
59
|
+
state.authorizor = block
|
55
60
|
end
|
56
61
|
|
57
62
|
def sign_out_with(method: :get, &block)
|
58
|
-
state
|
59
|
-
state
|
63
|
+
state.sign_out_method = method
|
64
|
+
state.sign_out_path = block
|
60
65
|
end
|
61
66
|
|
62
67
|
def sign_out_path
|
63
|
-
state
|
68
|
+
state.sign_out_path&.call
|
64
69
|
end
|
65
70
|
|
66
|
-
|
67
|
-
state[:sign_out_method]
|
68
|
-
end
|
71
|
+
delegate :sign_out_method, to: :state
|
69
72
|
|
70
73
|
def sign_in_with(method: :get, &block)
|
71
|
-
state
|
72
|
-
state
|
74
|
+
state.sign_in_method = method
|
75
|
+
state.sign_in_path = block
|
73
76
|
end
|
74
77
|
|
75
78
|
def sign_in_path
|
76
|
-
state
|
79
|
+
state.sign_in_path&.call
|
77
80
|
end
|
78
81
|
|
79
|
-
|
80
|
-
state[:sign_in_method]
|
81
|
-
end
|
82
|
+
delegate :sign_in_method, to: :state
|
82
83
|
|
83
84
|
def authenticate
|
84
85
|
authenticator&.call
|
85
|
-
@current_user = state
|
86
|
+
@current_user = state.authorizor&.call
|
86
87
|
|
87
88
|
nil
|
88
89
|
end
|
@@ -91,16 +92,12 @@ module ActiveElement
|
|
91
92
|
raise ArgumentError, "Must specify `with: '<permission>'` or `always: true`" unless with.present? || always
|
92
93
|
raise ArgumentError, 'Cannot specify both `with` and `always: true`' if with.present? && always
|
93
94
|
|
94
|
-
state
|
95
|
+
state.permissions << { with: with, always: always, action: action }
|
95
96
|
end
|
96
97
|
|
97
|
-
|
98
|
-
state[:authenticator]
|
99
|
-
end
|
98
|
+
delegate :authenticator, to: :state
|
100
99
|
|
101
|
-
|
102
|
-
state.fetch(:permissions)
|
103
|
-
end
|
100
|
+
delegate :permissions, to: :state
|
104
101
|
|
105
102
|
def component
|
106
103
|
return (@component ||= ActiveElement::Component.new(controller_instance)) unless controller_instance.nil?
|
@@ -116,8 +113,8 @@ module ActiveElement
|
|
116
113
|
|
117
114
|
attr_reader :controller_class, :controller_instance
|
118
115
|
|
119
|
-
def initialize_state
|
120
|
-
self.class.state[
|
116
|
+
def initialize_state(key)
|
117
|
+
self.class.state[key] ||= ControllerState.new(controller: controller_instance)
|
121
118
|
end
|
122
119
|
end
|
123
120
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Stores various data for a controller, including various field definitions and authentication
|
5
|
+
# configuration. Used throughout ActiveElement for generating dynamic content based on
|
6
|
+
# controller configuration.
|
7
|
+
class ControllerState
|
8
|
+
attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields
|
9
|
+
attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
|
10
|
+
:deletable, :authorizor, :authenticator, :list_order
|
11
|
+
|
12
|
+
def initialize(controller:)
|
13
|
+
@controller = controller
|
14
|
+
@permissions = []
|
15
|
+
@authenticator = nil
|
16
|
+
@authorizor = nil
|
17
|
+
@deletable = false
|
18
|
+
@listable_fields = []
|
19
|
+
@viewable_fields = []
|
20
|
+
@editable_fields = []
|
21
|
+
@searchable_fields = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def deletable?
|
25
|
+
!!deletable
|
26
|
+
end
|
27
|
+
|
28
|
+
def viewable?
|
29
|
+
viewable_fields.present? || controller.public_methods(false).include?(:show)
|
30
|
+
end
|
31
|
+
|
32
|
+
def editable?
|
33
|
+
editable_fields.present? || controller.public_methods(false).include?(:edit)
|
34
|
+
end
|
35
|
+
|
36
|
+
def creatable?
|
37
|
+
editable_fields.present? || controller.public_methods(false).include?(:new)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :controller
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,145 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module DefaultController
|
5
|
+
# Encapsulation of all logic performed for default controller actions when no action is defined
|
6
|
+
# by the current controller.
|
7
|
+
class Controller # rubocop:disable Metrics/ClassLength
|
8
|
+
def initialize(controller:)
|
9
|
+
@controller = controller
|
10
|
+
end
|
11
|
+
|
12
|
+
def index
|
13
|
+
return render_forbidden(:listable) unless configured?(:listable)
|
14
|
+
|
15
|
+
controller.render 'active_element/default_views/index',
|
16
|
+
locals: {
|
17
|
+
collection: ordered(collection),
|
18
|
+
search_filters: default_text_search.search_filters
|
19
|
+
}
|
20
|
+
end
|
21
|
+
|
22
|
+
def show
|
23
|
+
return render_forbidden(:viewable) unless configured?(:viewable)
|
24
|
+
|
25
|
+
controller.render 'active_element/default_views/show', locals: { record: record }
|
26
|
+
end
|
27
|
+
|
28
|
+
def new
|
29
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
30
|
+
|
31
|
+
controller.render 'active_element/default_views/new', locals: { record: model.new, namespace: namespace }
|
32
|
+
end
|
33
|
+
|
34
|
+
def create # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
35
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
36
|
+
|
37
|
+
new_record = model.new(default_record_params.params)
|
38
|
+
# XXX: Ensure associations are applied - there must be a better way.
|
39
|
+
if new_record.save && new_record.reload.update(default_record_params.params)
|
40
|
+
controller.flash.notice = "#{new_record.model_name.to_s.titleize} created successfully."
|
41
|
+
controller.redirect_to record_path(new_record, :show).path
|
42
|
+
else
|
43
|
+
controller.flash.now.alert = "Failed to create #{model.name.to_s.titleize}."
|
44
|
+
controller.render 'active_element/default_views/new', locals: { record: new_record, namespace: namespace }
|
45
|
+
end
|
46
|
+
rescue ActiveRecord::RangeError => e
|
47
|
+
render_range_error(error: e, action: :new)
|
48
|
+
end
|
49
|
+
|
50
|
+
def edit
|
51
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
52
|
+
|
53
|
+
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
54
|
+
end
|
55
|
+
|
56
|
+
def update # rubocop:disable Metrics/AbcSize
|
57
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
58
|
+
|
59
|
+
if record.update(default_record_params.params)
|
60
|
+
controller.flash.notice = "#{record.model_name.to_s.titleize} updated successfully."
|
61
|
+
controller.redirect_to record_path(record, :show).path
|
62
|
+
else
|
63
|
+
controller.flash.now.alert = "Failed to update #{model.name.to_s.titleize}."
|
64
|
+
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
65
|
+
end
|
66
|
+
rescue ActiveRecord::RangeError => e
|
67
|
+
render_range_error(error: e, action: :edit)
|
68
|
+
end
|
69
|
+
|
70
|
+
def destroy
|
71
|
+
return render_forbidden(:deletable) unless configured?(:deletable)
|
72
|
+
|
73
|
+
record.destroy
|
74
|
+
controller.flash.notice = "Deleted #{record.model_name.to_s.titleize}."
|
75
|
+
controller.redirect_to record_path(model, :index).path
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_reader :controller
|
81
|
+
|
82
|
+
def ordered(unordered_collection)
|
83
|
+
return unordered_collection if state.list_order.blank?
|
84
|
+
|
85
|
+
unordered_collection.order(state.list_order)
|
86
|
+
end
|
87
|
+
|
88
|
+
def render_forbidden(type)
|
89
|
+
controller.render 'active_element/default_views/forbidden', locals: { type: type }
|
90
|
+
end
|
91
|
+
|
92
|
+
def configured?(type)
|
93
|
+
return state.deletable? if type == :deletable
|
94
|
+
|
95
|
+
state.public_send("#{type}_fields").present?
|
96
|
+
end
|
97
|
+
|
98
|
+
def state
|
99
|
+
@state ||= controller.active_element.state
|
100
|
+
end
|
101
|
+
|
102
|
+
def default_record_params
|
103
|
+
@default_record_params ||= DefaultController::Params.new(controller: controller, model: model)
|
104
|
+
end
|
105
|
+
|
106
|
+
def default_text_search
|
107
|
+
@default_text_search ||= DefaultController::Search.new(controller: controller, model: model)
|
108
|
+
end
|
109
|
+
|
110
|
+
def record_path(record, type = nil)
|
111
|
+
ActiveElement::Components::Util::RecordPath.new(record: record, controller: controller, type: type)
|
112
|
+
end
|
113
|
+
|
114
|
+
def namespace
|
115
|
+
controller.controller_path.rpartition('/').first.presence&.to_sym
|
116
|
+
end
|
117
|
+
|
118
|
+
def model
|
119
|
+
controller.controller_name.classify.constantize
|
120
|
+
end
|
121
|
+
|
122
|
+
def record
|
123
|
+
@record ||= model.find(controller.params[:id])
|
124
|
+
end
|
125
|
+
|
126
|
+
def collection
|
127
|
+
return model.all unless default_text_search.text_search?
|
128
|
+
|
129
|
+
model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
|
130
|
+
end
|
131
|
+
|
132
|
+
def render_range_error(error:, action:)
|
133
|
+
controller.flash.now.alert = formatted_error(error)
|
134
|
+
controller.render "active_element/default_views/#{action}", locals: { record: record, namespace: namespace }
|
135
|
+
end
|
136
|
+
|
137
|
+
def formatted_error(error)
|
138
|
+
return error.cause.message.split("\n").join(', ') if error.try(:cause)&.try(:message).present?
|
139
|
+
return error.message if error.try(:message).present?
|
140
|
+
|
141
|
+
I18n.t('active_element.unexpected_error')
|
142
|
+
end
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
@@ -0,0 +1,48 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module DefaultController
|
5
|
+
# Provides permitted parameters for fields generated from a JSON schema file.
|
6
|
+
class JsonParams
|
7
|
+
def initialize(schema:)
|
8
|
+
@base_schema = schema
|
9
|
+
end
|
10
|
+
|
11
|
+
def params(schema = base_schema)
|
12
|
+
return simple_object_field(schema) if simple_object_field?(schema)
|
13
|
+
return simple_array_field(schema) if simple_array_field?(schema)
|
14
|
+
return complex_array_field(schema) if complex_array_field?(schema)
|
15
|
+
|
16
|
+
schema[:name]
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
attr_reader :fields, :base_schema
|
22
|
+
|
23
|
+
def simple_object_field(schema)
|
24
|
+
schema.key?(:name) ? { schema[:name] => {} } : {}
|
25
|
+
end
|
26
|
+
|
27
|
+
def simple_array_field(schema)
|
28
|
+
schema.key?(:name) ? { schema[:name] => [] } : []
|
29
|
+
end
|
30
|
+
|
31
|
+
def simple_object_field?(schema)
|
32
|
+
schema[:type] == 'object'
|
33
|
+
end
|
34
|
+
|
35
|
+
def simple_array_field?(schema)
|
36
|
+
schema[:type] == 'array' && schema.dig(:shape, :type) != 'object'
|
37
|
+
end
|
38
|
+
|
39
|
+
def complex_array_field?(schema)
|
40
|
+
schema[:type] == 'array' && schema.dig(:shape, :type) == 'object'
|
41
|
+
end
|
42
|
+
|
43
|
+
def complex_array_field(schema)
|
44
|
+
schema.dig(:shape, :shape, :fields).map { |field| params(field) }
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
@@ -0,0 +1,97 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module DefaultController
|
5
|
+
# Provides params for ActiveRecord models when using the default boilerplate controller
|
6
|
+
# actions. Navigates input parameters and maps them to appropriate relations as needed.
|
7
|
+
class Params
|
8
|
+
def initialize(controller:, model:)
|
9
|
+
@controller = controller
|
10
|
+
@model = model
|
11
|
+
end
|
12
|
+
|
13
|
+
def params
|
14
|
+
with_transformed_relations(
|
15
|
+
controller.params.require(controller.controller_name.singularize)
|
16
|
+
.permit(*permitted_fields)
|
17
|
+
)
|
18
|
+
end
|
19
|
+
|
20
|
+
private
|
21
|
+
|
22
|
+
attr_reader :controller, :model
|
23
|
+
|
24
|
+
def with_transformed_relations(params)
|
25
|
+
params.to_h.to_h do |key, value|
|
26
|
+
next [key, value] unless relation?(key)
|
27
|
+
|
28
|
+
relation_param(key, value)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def permitted_fields
|
33
|
+
scalar, json = controller.active_element.state.editable_fields.partition do |field|
|
34
|
+
scalar?(field)
|
35
|
+
end
|
36
|
+
scalar + [json_params(json)]
|
37
|
+
end
|
38
|
+
|
39
|
+
def scalar?(field)
|
40
|
+
return true if relation?(field)
|
41
|
+
return true if %i[json jsonb].exclude?(column(field)&.type)
|
42
|
+
|
43
|
+
false
|
44
|
+
end
|
45
|
+
|
46
|
+
def json_params(fields)
|
47
|
+
# XXX: We assume all non-scalar fields are JSON fields, i.e. they must have a definition
|
48
|
+
# defined as `config/forms/<model>/<field>.yml`. If that file does not exist, allow
|
49
|
+
# Errno::ENOENT to raise to let the form submission fail and avoid losing data. This
|
50
|
+
# would need to be adjusted if we start allowing non-JSON nested fields in the default
|
51
|
+
# controller.
|
52
|
+
fields.index_with do |field|
|
53
|
+
DefaultController::JsonParams.new(schema: schema_for(field)).params
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
def schema_for(field)
|
58
|
+
ActiveElement::Components::Util.json_schema(model: model, field: field)
|
59
|
+
end
|
60
|
+
|
61
|
+
def relation_param(key, value)
|
62
|
+
case relation(key).macro
|
63
|
+
when :belongs_to
|
64
|
+
belongs_to_param(key, value)
|
65
|
+
when :has_one
|
66
|
+
has_one_param(key, value)
|
67
|
+
when :has_many
|
68
|
+
has_many_param(key, value)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def belongs_to_param(key, value)
|
73
|
+
[relation(key).foreign_key, value]
|
74
|
+
end
|
75
|
+
|
76
|
+
def has_one_param(key, value) # rubocop:disable Naming/PredicateName
|
77
|
+
[relation(key).name, relation(key).klass.find_by(relation(key).klass.primary_key => value)]
|
78
|
+
end
|
79
|
+
|
80
|
+
def has_many_param(key, _value) # rubocop:disable Naming/PredicateName
|
81
|
+
[relation(key).name, relation(key).klass.where(relation(key).klass.primary_key => relation(key).value)]
|
82
|
+
end
|
83
|
+
|
84
|
+
def relation?(attribute)
|
85
|
+
relation(attribute.to_sym).present?
|
86
|
+
end
|
87
|
+
|
88
|
+
def relation(attribute)
|
89
|
+
model.reflect_on_association(attribute.to_sym)
|
90
|
+
end
|
91
|
+
|
92
|
+
def column(attribute)
|
93
|
+
model.columns.find { |column| column.name.to_s == attribute.to_s }
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
@@ -0,0 +1,112 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module DefaultController
|
5
|
+
# Full text search and datetime querying for DefaultController, provides full text search
|
6
|
+
# filters for all controllers with configured searchable fields. Includes support for querying
|
7
|
+
# across relations.
|
8
|
+
class Search
|
9
|
+
def initialize(controller:, model:)
|
10
|
+
@controller = controller
|
11
|
+
@model = model
|
12
|
+
end
|
13
|
+
|
14
|
+
def search_filters
|
15
|
+
@search_filters ||= controller.params.permit(*searchable_fields).transform_values do |value|
|
16
|
+
value.try(:compact_blank) || value
|
17
|
+
end.compact_blank
|
18
|
+
end
|
19
|
+
|
20
|
+
def text_search?
|
21
|
+
search_filters.present?
|
22
|
+
end
|
23
|
+
|
24
|
+
def text_search
|
25
|
+
conditions = search_filters.to_h.map do |key, value|
|
26
|
+
next relation_matches(key, value) if relation?(key)
|
27
|
+
next datetime_between(key, value) if datetime?(key)
|
28
|
+
|
29
|
+
model.arel_table[key].matches("#{value}%")
|
30
|
+
end
|
31
|
+
conditions[1..].reduce(conditions.first) do |accumulated, condition|
|
32
|
+
accumulated.and(condition)
|
33
|
+
end
|
34
|
+
end
|
35
|
+
|
36
|
+
def search_relations
|
37
|
+
search_filters.to_h.keys.map { |key| relation?(key) ? key.to_sym : nil }.compact
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :controller, :model
|
43
|
+
|
44
|
+
def searchable_fields
|
45
|
+
controller.active_element.state.searchable_fields.map do |field|
|
46
|
+
next field unless field.to_s.end_with?('_at')
|
47
|
+
|
48
|
+
{ field => %i[from to] }
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def noop
|
53
|
+
Arel::Nodes::True.new.eq(Arel::Nodes::True.new)
|
54
|
+
end
|
55
|
+
|
56
|
+
def datetime?(key)
|
57
|
+
model.columns.find { |column| column.name.to_s == key.to_s }&.type == :datetime
|
58
|
+
end
|
59
|
+
|
60
|
+
def datetime_between(key, value)
|
61
|
+
return noop if value[:from].blank? && value[:to].blank?
|
62
|
+
|
63
|
+
model.arel_table[key].between(range_begin(value)...range_end(value))
|
64
|
+
end
|
65
|
+
|
66
|
+
def range_begin(value)
|
67
|
+
value[:from].present? ? Time.zone.parse(value[:from]) + timezone_offset : -Float::INFINITY
|
68
|
+
end
|
69
|
+
|
70
|
+
def range_end(value)
|
71
|
+
value[:to].present? ? Time.zone.parse(value[:to]) + timezone_offset : Float::INFINITY
|
72
|
+
end
|
73
|
+
|
74
|
+
def timezone_offset
|
75
|
+
controller.request.cookies['timezone_offset'].to_i.minutes
|
76
|
+
end
|
77
|
+
|
78
|
+
def relation_matches(key, value)
|
79
|
+
fields = searchable_relation_fields(key)
|
80
|
+
relation_model = relation(key).klass
|
81
|
+
fields.select! do |field|
|
82
|
+
relation_model.columns.find { |column| column.name.to_s == field.to_s }&.type == :string
|
83
|
+
end
|
84
|
+
|
85
|
+
return noop if fields.empty?
|
86
|
+
|
87
|
+
relation_conditions(fields, value, relation_model)
|
88
|
+
end
|
89
|
+
|
90
|
+
def relation_conditions(fields, value, relation_model)
|
91
|
+
fields[1..].reduce(relation_model.arel_table[fields.first].matches("#{value}%")) do |condition, field|
|
92
|
+
condition.or(relation_model.arel_table[field].matches("#{value}%"))
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def searchable_relation_fields(key)
|
97
|
+
Components::Util.relation_controller(model, controller, key)
|
98
|
+
&.active_element
|
99
|
+
&.state
|
100
|
+
&.fetch(:searchable_fields, []) || []
|
101
|
+
end
|
102
|
+
|
103
|
+
def relation?(attribute)
|
104
|
+
relation(attribute.to_sym).present?
|
105
|
+
end
|
106
|
+
|
107
|
+
def relation(attribute)
|
108
|
+
model.reflect_on_association(attribute.to_sym)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
@@ -1,93 +1,15 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
@controller = controller
|
9
|
-
end
|
10
|
-
|
11
|
-
def index
|
12
|
-
controller.render 'active_element/default_views/index',
|
13
|
-
locals: {
|
14
|
-
collection: collection,
|
15
|
-
search_filters: default_text_search.search_filters
|
16
|
-
}
|
17
|
-
end
|
18
|
-
|
19
|
-
def show
|
20
|
-
controller.render 'active_element/default_views/show', locals: { record: record }
|
21
|
-
end
|
22
|
-
|
23
|
-
def new
|
24
|
-
controller.render 'active_element/default_views/new', locals: { record: model.new, namespace: namespace }
|
25
|
-
end
|
26
|
-
|
27
|
-
def create # rubocop:disable Metrics/AbcSize
|
28
|
-
new_record = model.new(default_record_params.params)
|
29
|
-
# Ensure associations are applied:
|
30
|
-
if new_record.save && new_record.reload.update(default_record_params.params)
|
31
|
-
controller.flash.notice = "#{new_record.model_name.to_s.titleize} created successfully."
|
32
|
-
controller.redirect_to record_path(new_record, :show).path
|
33
|
-
else
|
34
|
-
controller.flash.now.alert = "Failed to create #{model.name.to_s.titleize}."
|
35
|
-
controller.render 'active_element/default_views/new', locals: { record: new_record, namespace: namespace }
|
36
|
-
end
|
37
|
-
end
|
38
|
-
|
39
|
-
def edit
|
40
|
-
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
41
|
-
end
|
42
|
-
|
43
|
-
def update # rubocop:disable Metrics/AbcSize
|
44
|
-
if record.update(default_record_params.params)
|
45
|
-
controller.flash.notice = "#{record.model_name.to_s.titleize} updated successfully."
|
46
|
-
controller.redirect_to record_path(record, :show).path
|
47
|
-
else
|
48
|
-
controller.flash.now.alert = "Failed to update #{model.name.to_s.titleize}."
|
49
|
-
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
50
|
-
end
|
51
|
-
end
|
52
|
-
|
53
|
-
def destroy
|
54
|
-
record.destroy
|
55
|
-
controller.flash.notice = "Deleted #{record.model_name.to_s.titleize}."
|
56
|
-
controller.redirect_to record_path(model, :index).path
|
57
|
-
end
|
58
|
-
|
59
|
-
private
|
3
|
+
require_relative 'default_controller/controller'
|
4
|
+
require_relative 'default_controller/actions'
|
5
|
+
require_relative 'default_controller/params'
|
6
|
+
require_relative 'default_controller/json_params'
|
7
|
+
require_relative 'default_controller/search'
|
60
8
|
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
def default_text_search
|
68
|
-
@default_text_search ||= ActiveElement::DefaultTextSearch.new(controller: controller, model: model)
|
69
|
-
end
|
70
|
-
|
71
|
-
def record_path(record, type = nil)
|
72
|
-
ActiveElement::Components::Util::RecordPath.new(record: record, controller: controller, type: type)
|
73
|
-
end
|
74
|
-
|
75
|
-
def namespace
|
76
|
-
controller.controller_path.rpartition('/').first.presence&.to_sym
|
77
|
-
end
|
78
|
-
|
79
|
-
def model
|
80
|
-
controller.controller_name.classify.constantize
|
81
|
-
end
|
82
|
-
|
83
|
-
def record
|
84
|
-
@record ||= model.find(controller.params[:id])
|
85
|
-
end
|
86
|
-
|
87
|
-
def collection
|
88
|
-
return model.all unless default_text_search.text_search?
|
89
|
-
|
90
|
-
model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
|
91
|
-
end
|
9
|
+
module ActiveElement
|
10
|
+
# Provides default boilerplate functionality for quick setup of an application.
|
11
|
+
# Implements all standard Rails controller actions, provides parameter permitting of configured
|
12
|
+
# fields and text search functionality.
|
13
|
+
module DefaultController
|
92
14
|
end
|
93
15
|
end
|