active_element 0.0.12 → 0.0.13
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile +1 -1
- data/Gemfile.lock +3 -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/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 +7 -0
- data/app/views/active_element/components/form.html.erb +2 -2
- data/app/views/layouts/active_element.html.erb +25 -4
- data/example_app/Gemfile.lock +1 -1
- data/lib/active_element/components/form.rb +1 -8
- data/lib/active_element/components/util/display_value_mapping.rb +0 -2
- data/lib/active_element/components/util/form_field_mapping.rb +2 -1
- data/lib/active_element/components/util.rb +7 -0
- data/lib/active_element/controller_interface.rb +2 -1
- data/lib/active_element/controller_state.rb +1 -1
- 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 -132
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +0 -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 +11 -4
- data/lib/active_element/default_record_params.rb +0 -62
- data/lib/active_element/default_search.rb +0 -110
@@ -2,12 +2,23 @@
|
|
2
2
|
@import "bootstrap";
|
3
3
|
@import "dark";
|
4
4
|
|
5
|
+
|
6
|
+
@keyframes fade-in {
|
7
|
+
from {
|
8
|
+
opacity: 0;
|
9
|
+
}
|
10
|
+
|
11
|
+
to {
|
12
|
+
opacity: 1;
|
13
|
+
}
|
14
|
+
}
|
15
|
+
|
5
16
|
.application-menu {
|
6
17
|
height: 5rem;
|
7
18
|
padding-left: 2rem;
|
8
19
|
top: 0;
|
9
20
|
transition: height 0.5s ease-in-out, background-position 0.8s ease-in-out;
|
10
|
-
background-color: #
|
21
|
+
background-color: #508ea1 !important;
|
11
22
|
z-index: 2000;
|
12
23
|
.dropdown-toggle::after {
|
13
24
|
color: #{$blue};
|
@@ -77,6 +88,12 @@ form {
|
|
77
88
|
margin-top: 5rem;
|
78
89
|
}
|
79
90
|
|
91
|
+
.modal-content pre, .modal-content div.json-highlight {
|
92
|
+
font-size: 0.875rem;
|
93
|
+
line-height: 1.2rem;
|
94
|
+
font-family: monospace;
|
95
|
+
}
|
96
|
+
|
80
97
|
.json-field {
|
81
98
|
ol.json-array-field {
|
82
99
|
margin-top: 1rem;
|
@@ -105,6 +122,11 @@ form {
|
|
105
122
|
.form-group {
|
106
123
|
padding: 1rem;
|
107
124
|
background-color: #58575755;
|
125
|
+
|
126
|
+
&.depth-1 {
|
127
|
+
padding: 0;
|
128
|
+
background-color: transparent;
|
129
|
+
}
|
108
130
|
}
|
109
131
|
|
110
132
|
.form-check {
|
@@ -176,6 +198,16 @@ form {
|
|
176
198
|
}
|
177
199
|
|
178
200
|
|
201
|
+
.json-array-field {
|
202
|
+
li, .json-delete-button {
|
203
|
+
opacity: 0;
|
204
|
+
animation: fade-in ease-in 1;
|
205
|
+
animation-fill-mode: forwards;
|
206
|
+
animation-duration: 0.5s;
|
207
|
+
animation-delay: 0;
|
208
|
+
}
|
209
|
+
}
|
210
|
+
|
179
211
|
.json-array-field {
|
180
212
|
li .json-text-field,
|
181
213
|
li .json-select-field,
|
@@ -185,12 +217,18 @@ form {
|
|
185
217
|
li .json-date-field,
|
186
218
|
li .json-time-field,
|
187
219
|
li .json-datetime-field {
|
220
|
+
|
188
221
|
&.deletable {
|
189
222
|
width: calc(100% - 3.2rem);
|
190
223
|
}
|
191
224
|
}
|
192
225
|
}
|
193
226
|
|
227
|
+
.append-button {
|
228
|
+
position: relative;
|
229
|
+
z-index: 300;
|
230
|
+
}
|
231
|
+
|
194
232
|
.form-control, .form-select {
|
195
233
|
display: inline;
|
196
234
|
.append-button {
|
@@ -8,31 +8,31 @@ module ActiveElement
|
|
8
8
|
extend ActiveSupport::Concern
|
9
9
|
|
10
10
|
def index
|
11
|
-
ActiveElement::DefaultController.new(controller: self).index
|
11
|
+
ActiveElement::DefaultController::Controller.new(controller: self).index
|
12
12
|
end
|
13
13
|
|
14
14
|
def show
|
15
|
-
ActiveElement::DefaultController.new(controller: self).show
|
15
|
+
ActiveElement::DefaultController::Controller.new(controller: self).show
|
16
16
|
end
|
17
17
|
|
18
18
|
def new
|
19
|
-
ActiveElement::DefaultController.new(controller: self).new
|
19
|
+
ActiveElement::DefaultController::Controller.new(controller: self).new
|
20
20
|
end
|
21
21
|
|
22
22
|
def create
|
23
|
-
ActiveElement::DefaultController.new(controller: self).create
|
23
|
+
ActiveElement::DefaultController::Controller.new(controller: self).create
|
24
24
|
end
|
25
25
|
|
26
26
|
def edit
|
27
|
-
ActiveElement::DefaultController.new(controller: self).edit
|
27
|
+
ActiveElement::DefaultController::Controller.new(controller: self).edit
|
28
28
|
end
|
29
29
|
|
30
30
|
def update
|
31
|
-
ActiveElement::DefaultController.new(controller: self).update
|
31
|
+
ActiveElement::DefaultController::Controller.new(controller: self).update
|
32
32
|
end
|
33
33
|
|
34
34
|
def destroy
|
35
|
-
ActiveElement::DefaultController.new(controller: self).destroy
|
35
|
+
ActiveElement::DefaultController::Controller.new(controller: self).destroy
|
36
36
|
end
|
37
37
|
end
|
38
38
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
<a data-modal-id="<%= "#json-modal-#{field_id}" %>"
|
2
|
+
id="<%= "json-view-modal-trigger-#{field_id}" %>"
|
2
3
|
data-json-modal-link="true"
|
3
4
|
data-bs-toggle="modal"
|
4
5
|
data-bs-target="#json-modal-<%= field_id %>"
|
5
|
-
class="text-decoration-none"
|
6
|
-
href="#">
|
6
|
+
class="text-decoration-none text-nowrap"
|
7
|
+
href="#">JSON <i class="fa-solid fa-magnifying-glass"></i></a>
|
7
8
|
<div id="json-modal-<%= field_id %>" class="modal fade"
|
8
9
|
tabindex="-1"
|
9
10
|
aria-hidden="true">
|
@@ -6,7 +6,8 @@
|
|
6
6
|
locals: { form_id: id, form: form, field: field, options: options, component: component } %>
|
7
7
|
<% elsif type == :json_field %>
|
8
8
|
<%= render partial: 'active_element/components/form/json',
|
9
|
-
locals: { form_id: id, form: form, field: field, field_id:
|
9
|
+
locals: { form_id: id, form: form, field: field, field_id: "#{id}-json-field-#{field}",
|
10
|
+
options: options, component: component } %>
|
10
11
|
<% elsif type == :text_search_field %>
|
11
12
|
<%= render partial: 'active_element/components/form/text_search',
|
12
13
|
locals: { form_id: id, form: form, field: field, options: options, component: component } %>
|
@@ -4,6 +4,8 @@
|
|
4
4
|
data-form-id="<%= form_id %>"
|
5
5
|
data-field-id="<%= field_id %>"
|
6
6
|
data-schema-field-id="<%= field_id %>-schema"
|
7
|
+
data-json-view-modal-id="<%= "json-modal-#{form_id}-#{field}-json-view" %>"
|
8
|
+
data-json-view-modal-trigger-id="<%= "json-view-modal-trigger-#{form_id}-#{field}-json-view" %>"
|
7
9
|
>
|
8
10
|
|
9
11
|
</div>
|
@@ -26,3 +26,10 @@
|
|
26
26
|
<%= render partial: 'active_element/components/form/option_groups_summary',
|
27
27
|
locals: { option_groups: options[:option_groups] } %>
|
28
28
|
<% end %>
|
29
|
+
|
30
|
+
<% if type == :json_field %>
|
31
|
+
<div>
|
32
|
+
<%= render partial: 'active_element/components/fields/json',
|
33
|
+
locals: { value: component.value_for(field), field_id: "#{id}-#{field}-json-view" } %>
|
34
|
+
</div>
|
35
|
+
<% end %>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<% if destroy %>
|
2
|
-
<div class="container w-100 text-end">
|
2
|
+
<div class="container me-0 w-100 text-end">
|
3
3
|
<%= active_element.component.destroy_button(record) %>
|
4
4
|
</div>
|
5
5
|
<% end %>
|
@@ -51,7 +51,7 @@
|
|
51
51
|
<% field_group.each do |field, type, options| %>
|
52
52
|
<div class="col-sm-3">
|
53
53
|
<%= render partial: 'active_element/components/form/label',
|
54
|
-
locals: { type: type, form: form, field: field, options: options } %>
|
54
|
+
locals: { component: component, id: id, type: type, form: form, field: field, options: options } %>
|
55
55
|
</div>
|
56
56
|
|
57
57
|
|
@@ -2,6 +2,13 @@
|
|
2
2
|
<head>
|
3
3
|
<%= render_active_element_hook 'active_element/before_head' %>
|
4
4
|
|
5
|
+
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css" integrity="sha512-iecdLmaskl7CVkqkXNQ/ZH/XLlvWZOJyj7Yy7tcenmpD1ypASozpmT/E0iPtmFIB46ZmdtAc9eNBvH0H/ZpiBw==" crossorigin="anonymous" referrerpolicy="no-referrer" />
|
6
|
+
<link rel="stylesheet"
|
7
|
+
href="//cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/styles/default.min.css">
|
8
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/js/all.min.js" integrity="sha512-fD9DI5bZwQxOi7MhYWnnNPlvXdp/2Pj3XSTRrFs5FQa4mizyGLnJcN6tuvUS6LbmgN1ut+XGSABKvjN0H6Aoow==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
9
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/popper.js/2.9.2/umd/popper.min.js" integrity="sha512-2rNj2KJ+D8s1ceNasTIex6z4HWyOnEYLVC3FigGOmyQCZc2eBXKgOxQmo3oKLHyfcj53uz4QMsRCWNbLd32Q1g==" crossorigin="anonymous" referrerpolicy="no-referrer"></script>
|
10
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/highlight.js/11.7.0/highlight.min.js"></script>
|
11
|
+
|
5
12
|
<style>
|
6
13
|
<%= Rouge::Theme.find('tulip').render(scope: '.json-highlight') %>
|
7
14
|
.json-highlight .p {
|
@@ -12,14 +19,28 @@
|
|
12
19
|
color: #6b7399
|
13
20
|
}
|
14
21
|
|
22
|
+
.json-highlight .kc, .json-highlight .c {
|
23
|
+
color: #695;
|
24
|
+
}
|
25
|
+
|
15
26
|
.json-highlight {
|
16
27
|
background-color: transparent;
|
17
28
|
}
|
18
|
-
</style>
|
19
29
|
|
20
|
-
|
21
|
-
|
22
|
-
|
30
|
+
.hljs-punctuation {
|
31
|
+
color: #7e4b6f;
|
32
|
+
}
|
33
|
+
|
34
|
+
.hljs-attr {
|
35
|
+
color: #9f93e6;
|
36
|
+
font-weight: bold;
|
37
|
+
}
|
38
|
+
|
39
|
+
.hljs-string {
|
40
|
+
color: #6b7399;
|
41
|
+
font-weight: bold;
|
42
|
+
}
|
43
|
+
</style>
|
23
44
|
|
24
45
|
<script>
|
25
46
|
window.ActiveElement = window.ActiveElement || {};
|
data/example_app/Gemfile.lock
CHANGED
@@ -81,14 +81,7 @@ module ActiveElement
|
|
81
81
|
end
|
82
82
|
|
83
83
|
def schema_for(field, options)
|
84
|
-
options.key?(:schema) ? options.fetch(:schema) :
|
85
|
-
end
|
86
|
-
|
87
|
-
def schema_from_yaml(field)
|
88
|
-
YAML.safe_load(
|
89
|
-
Rails.root.join("config/forms/#{record.class.name.underscore}/#{field}.yml").read,
|
90
|
-
symbolize_names: true
|
91
|
-
)
|
84
|
+
options.key?(:schema) ? options.fetch(:schema) : Util.json_schema(model: record.class, field: field)
|
92
85
|
end
|
93
86
|
|
94
87
|
def display_value_for_select(field, options)
|
@@ -16,8 +16,6 @@ module ActiveElement
|
|
16
16
|
end
|
17
17
|
|
18
18
|
def json_value
|
19
|
-
return ActiveElement.json_pretty_print(value_from_record) unless component.is_a?(CollectionTable)
|
20
|
-
|
21
19
|
component.controller.render_to_string(
|
22
20
|
partial: 'active_element/components/fields/json',
|
23
21
|
locals: { value: value_from_record, field_id: ActiveElement.element_id }
|
@@ -136,8 +136,9 @@ module ActiveElement
|
|
136
136
|
end
|
137
137
|
|
138
138
|
def searchable_fields(field)
|
139
|
+
# FIXME: Use database column type to only include strings/numbers.
|
139
140
|
(Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || [])
|
140
|
-
.reject { |
|
141
|
+
.reject { |searchable_field| searchable_field.to_s.end_with?('_at') }
|
141
142
|
end
|
142
143
|
|
143
144
|
def relation_primary_key(field)
|
@@ -41,6 +41,13 @@ module ActiveElement
|
|
41
41
|
"#{namespace.classify}::#{base}".safe_constantize || base.safe_constantize
|
42
42
|
end
|
43
43
|
|
44
|
+
def self.json_schema(model:, field:)
|
45
|
+
YAML.safe_load(
|
46
|
+
Rails.root.join("config/forms/#{model.name.underscore}/#{field}.yml").read,
|
47
|
+
symbolize_names: true
|
48
|
+
)
|
49
|
+
end
|
50
|
+
|
44
51
|
def self.json_pretty_print(json)
|
45
52
|
formatter = Rouge::Formatters::HTML.new
|
46
53
|
lexer = Rouge::Lexers::JSON.new
|
@@ -7,7 +7,7 @@ module ActiveElement
|
|
7
7
|
class ControllerState
|
8
8
|
attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields
|
9
9
|
attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
|
10
|
-
:deletable, :authorizor, :authenticator
|
10
|
+
:deletable, :authorizor, :authenticator, :list_order
|
11
11
|
|
12
12
|
def initialize(controller:)
|
13
13
|
@controller = controller
|
@@ -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
|