active_element 0.0.12 → 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/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
|