active_element 0.0.11 → 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/.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
@@ -71,7 +71,16 @@
|
|
71
71
|
const token = ActiveElement.getAntiCsrfToken();
|
72
72
|
const responseErrorContainer = cloneElement('response-error');
|
73
73
|
const searchResultsContainer = cloneElement('results');
|
74
|
-
const
|
74
|
+
const icons = cloneElement('icons');
|
75
|
+
const spinner = icons.querySelector('[data-item-class="spinner"]');
|
76
|
+
const clearButton = icons.querySelector('[data-item-class="clear"]');
|
77
|
+
|
78
|
+
if (element.value) clearButton.classList.remove('invisible');
|
79
|
+
clearButton.addEventListener('click', () => {
|
80
|
+
element.value = '';
|
81
|
+
hiddenInput.value = '';
|
82
|
+
clearButton.classList.add('invisible');
|
83
|
+
});
|
75
84
|
|
76
85
|
document.addEventListener('click', () => {
|
77
86
|
searchResultsContainer.classList.add('d-none');
|
@@ -79,6 +88,7 @@
|
|
79
88
|
|
80
89
|
element.addEventListener('change', () => {
|
81
90
|
hiddenInput.value = element.value;
|
91
|
+
if (element.value) clearButton.classList.remove('invisible');
|
82
92
|
});
|
83
93
|
|
84
94
|
element.addEventListener('keyup', () => {
|
@@ -87,6 +97,7 @@
|
|
87
97
|
lastRequestId = requestId;
|
88
98
|
|
89
99
|
spinner.classList.remove('invisible');
|
100
|
+
if (element.value) clearButton.classList.remove('invisible');
|
90
101
|
searchResultsContainer.classList.add('d-none');
|
91
102
|
|
92
103
|
if (!query || query.length < 3) {
|
@@ -119,6 +130,7 @@
|
|
119
130
|
|
120
131
|
form.append(hiddenInput);
|
121
132
|
element.parentElement.append(searchResultsContainer);
|
133
|
+
element.parentElement.append(clearButton);
|
122
134
|
element.parentElement.append(spinner);
|
123
135
|
element.parentElement.append(responseErrorContainer);
|
124
136
|
});
|
@@ -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>
|
@@ -6,7 +6,7 @@
|
|
6
6
|
data-bs-trigger="focus"
|
7
7
|
data-bs-toggle="popover"
|
8
8
|
title="Required"
|
9
|
-
data-bs-content="<%= "#{options.fetch(:label)} is a required field." %>"
|
9
|
+
data-bs-content="<%= "#{options.fetch(:label)} is a required field." %>">
|
10
10
|
<i class="text-secondary fa-solid fa-star-of-life"></i>
|
11
11
|
</button>
|
12
12
|
<% end %>
|
@@ -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 %>
|
@@ -18,8 +18,8 @@
|
|
18
18
|
<input id="json-time-field-template" type="time" class="form-control m-1 d-inline-block json-time-field" />
|
19
19
|
<input id="json-datetime-field-template" type="datetime-local" class="form-control m-1 d-inline-block json-datetime-field" />
|
20
20
|
<input id="json-integer-field-template" type="number" class="form-control m-1 json-integer-field" />
|
21
|
-
<input id="json-float-field-template" type="number" class="form-control m-1 json-float-field" />
|
22
|
-
<input id="json-decimal-field-template" type="number" class="form-control m-1 json-decimal-field" />
|
21
|
+
<input id="json-float-field-template" type="number" step="any" class="form-control m-1 json-float-field" />
|
22
|
+
<input id="json-decimal-field-template" type="number" step="any" class="form-control m-1 json-decimal-field" />
|
23
23
|
|
24
24
|
<select id="json-select-template" class="form-select m-1 json-select-field"></select>
|
25
25
|
|
@@ -65,9 +65,12 @@
|
|
65
65
|
<p id="form-search-field-response-error-template" class="text-danger validation-error-message pt-1 m-0"></p>
|
66
66
|
<div id="form-search-field-results-template" class="search-field-results d-none border border-top-0"></div>
|
67
67
|
<div id="form-search-field-results-item-template" class="search-field-result p-2"></div>
|
68
|
-
<div id="form-search-field-
|
69
|
-
<div style="position: relative; float: right; width: auto; right: 0.5rem; top: -1.7rem;">
|
70
|
-
<i class="fa-solid fa-spinner fa-spin"></i>
|
68
|
+
<div id="form-search-field-icons-template" class="text-end">
|
69
|
+
<div data-item-class="spinner" class="invisible" style="position: relative; float: right; width: auto; right: 0.5rem; top: -1.7rem;">
|
70
|
+
<i class="fa-fw fa-solid fa-spinner fa-spin"></i>
|
71
|
+
</div>
|
72
|
+
<div data-item-class="clear" class="invisible" style="position: relative; float: right; width: auto; right: 0.5rem; top: -1.7rem; cursor: pointer;">
|
73
|
+
<i class="fa-solid fa-fw fa-xmark"></i>
|
71
74
|
</div>
|
72
75
|
</div>
|
73
76
|
</div>
|
@@ -3,7 +3,7 @@
|
|
3
3
|
id: "#{form_id}-#{field}-text-search",
|
4
4
|
class: "form-control #{component.valid?(field) ? nil : 'is-invalid'}",
|
5
5
|
autocomplete: 'off',
|
6
|
-
placeholder: options[:placeholder].presence ||
|
6
|
+
placeholder: options[:placeholder].presence || "Search...",
|
7
7
|
tabindex: component.tabindex,
|
8
8
|
data: {
|
9
9
|
field_type: 'text-search',
|
@@ -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
|
|
@@ -8,6 +8,7 @@
|
|
8
8
|
<button type="button"
|
9
9
|
style="background: none; border: none; outline: 0; position: absolute; margin-top: 0.3rem"
|
10
10
|
data-bs-toggle="popover"
|
11
|
+
data-bs-trigger="focus"
|
11
12
|
data-bs-content="<%= options[:description] %>">
|
12
13
|
<i class="text-secondary fa-solid fa-circle-info"></i>
|
13
14
|
</button>
|
@@ -20,6 +20,7 @@
|
|
20
20
|
<button type="button"
|
21
21
|
style="background: none; border: none; outline: 0; position: absolute; margin-top: 0.3rem"
|
22
22
|
data-bs-toggle="popover"
|
23
|
+
data-bs-trigger="focus"
|
23
24
|
data-bs-content="<%= options[:description] %>">
|
24
25
|
<i class="text-secondary fa-solid fa-circle-info"></i>
|
25
26
|
</button>
|
@@ -1,5 +1,5 @@
|
|
1
1
|
<%= active_element.component.page_title record.model_name.to_s.titleize %>
|
2
2
|
|
3
3
|
<%= active_element.component.form model: [namespace, record].compact,
|
4
|
-
destroy:
|
5
|
-
fields: active_element.state.
|
4
|
+
destroy: active_element.state.deletable?,
|
5
|
+
fields: active_element.state.editable_fields %>
|
@@ -0,0 +1,7 @@
|
|
1
|
+
<h1>Forbidden</h1>
|
2
|
+
|
3
|
+
<h2 class="text-danger">Access to this resource has not been configured</h2>
|
4
|
+
|
5
|
+
<p>The <span class="text-primary font-monospace"><%= type %></span> resource for <span class="text-primary font-monospace"><%= controller_name.titleize %></span> has not been configured, please contact your administrator.</p>
|
6
|
+
|
7
|
+
<hr/>
|
@@ -1,15 +1,15 @@
|
|
1
|
-
<% if active_element.state.
|
1
|
+
<% if active_element.state.searchable_fields.present? %>
|
2
2
|
<%= active_element.component.form title: 'Search Filters',
|
3
3
|
submit: 'Search',
|
4
4
|
modal: true,
|
5
5
|
search: true,
|
6
6
|
item: search_filters,
|
7
|
-
fields: active_element.state.
|
7
|
+
fields: active_element.state.searchable_fields %>
|
8
8
|
<% end %>
|
9
9
|
|
10
|
-
<%= active_element.component.table new:
|
11
|
-
show:
|
12
|
-
edit:
|
13
|
-
destroy:
|
10
|
+
<%= active_element.component.table new: active_element.state.creatable?,
|
11
|
+
show: active_element.state.viewable?,
|
12
|
+
edit: active_element.state.editable?,
|
13
|
+
destroy: active_element.state.deletable?,
|
14
14
|
collection: collection,
|
15
|
-
fields: active_element.state.
|
15
|
+
fields: active_element.state.listable_fields %>
|
@@ -1,7 +1,7 @@
|
|
1
1
|
<%= active_element.component.page_title record.model_name.to_s.titleize %>
|
2
2
|
|
3
3
|
<%= active_element.component.table item: record,
|
4
|
-
edit:
|
5
|
-
destroy:
|
6
|
-
fields: active_element.state.
|
4
|
+
edit: active_element.state.editable?,
|
5
|
+
destroy: active_element.state.deletable?,
|
6
|
+
fields: active_element.state.viewable_fields %>
|
7
7
|
|
@@ -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
@@ -3,4 +3,5 @@ class PetsController < ApplicationController
|
|
3
3
|
active_element.viewable_fields :name, :animal, :owner, :created_at, :updated_at
|
4
4
|
active_element.editable_fields :name, :animal, :owner
|
5
5
|
active_element.searchable_fields :name, :animal, :owner, :created_at, :updated_at
|
6
|
+
active_element.deletable
|
6
7
|
end
|
@@ -3,4 +3,5 @@ class UsersController < ApplicationController
|
|
3
3
|
active_element.viewable_fields :name, :email, :created_at, :updated_at, :pets
|
4
4
|
active_element.listable_fields :name, :email, :created_at, :updated_at
|
5
5
|
active_element.searchable_fields :name, :email, :created_at, :updated_at
|
6
|
+
active_element.deletable
|
6
7
|
end
|
@@ -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)
|
@@ -19,7 +19,16 @@ module ActiveElement
|
|
19
19
|
end
|
20
20
|
|
21
21
|
def text_search_options(model:, with:, providing:)
|
22
|
-
{
|
22
|
+
{
|
23
|
+
search: { model: model.name.underscore, with: with, providing: providing },
|
24
|
+
placeholder: "Search for #{model.name.titleize} by #{humanized_names(with).join(', ')}..."
|
25
|
+
}
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def humanized_names(names)
|
31
|
+
Array(names).compact.map.map(&:to_s).map(&:humanize)
|
23
32
|
end
|
24
33
|
end
|
25
34
|
end
|
@@ -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 }
|
@@ -127,21 +127,22 @@ module ActiveElement
|
|
127
127
|
end
|
128
128
|
|
129
129
|
def relation_text_search_field(field)
|
130
|
-
relation_model = relation(field).klass
|
131
|
-
record.public_send(field)
|
132
130
|
[field, :text_search_field,
|
133
131
|
TextSearch.text_search_options(
|
134
|
-
model:
|
132
|
+
model: relation(field).klass,
|
135
133
|
with: searchable_fields(field),
|
136
|
-
providing:
|
137
|
-
).merge({ display_value: association_mapping(field).display_value })]
|
134
|
+
providing: relation(field).klass.primary_key
|
135
|
+
).merge({ display_value: association_mapping(field).display_value, label: i18n.label(field) })]
|
138
136
|
end
|
139
137
|
|
140
138
|
def searchable_fields(field)
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
139
|
+
# FIXME: Use database column type to only include strings/numbers.
|
140
|
+
(Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || [])
|
141
|
+
.reject { |searchable_field| searchable_field.to_s.end_with?('_at') }
|
142
|
+
end
|
143
|
+
|
144
|
+
def relation_primary_key(field)
|
145
|
+
relation(field).options.fetch(:primary_key) { relation_model.primary_key }
|
145
146
|
end
|
146
147
|
|
147
148
|
def default_type_from_column_type(field, column_type) # rubocop:disable Metrics/MethodLength
|
@@ -204,7 +205,9 @@ module ActiveElement
|
|
204
205
|
end
|
205
206
|
|
206
207
|
def default_options(field)
|
207
|
-
{
|
208
|
+
{
|
209
|
+
required: required?(field)
|
210
|
+
}.merge(field_options(field))
|
208
211
|
end
|
209
212
|
|
210
213
|
def required?(field)
|
@@ -215,6 +218,16 @@ module ActiveElement
|
|
215
218
|
validator.kind == :presence && validator.attributes.include?(field.to_sym)
|
216
219
|
end
|
217
220
|
end
|
221
|
+
|
222
|
+
def field_options(field)
|
223
|
+
return NumericField.new(field: field, column: column(field)).options if numeric?(field)
|
224
|
+
|
225
|
+
{}
|
226
|
+
end
|
227
|
+
|
228
|
+
def numeric?(field)
|
229
|
+
%i[float decimal integer].include?(column(field)&.type)
|
230
|
+
end
|
218
231
|
end
|
219
232
|
end
|
220
233
|
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
module Components
|
5
|
+
module Util
|
6
|
+
# Provides options for a `<input type="number" />` element based on database column properties.
|
7
|
+
class NumericField
|
8
|
+
def initialize(field:, column:)
|
9
|
+
@field = field
|
10
|
+
@column = column
|
11
|
+
end
|
12
|
+
|
13
|
+
def options
|
14
|
+
{
|
15
|
+
step: step,
|
16
|
+
min: min,
|
17
|
+
max: max
|
18
|
+
}.compact
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
attr_reader :field, :column
|
24
|
+
|
25
|
+
def step
|
26
|
+
return 'any' if column.blank?
|
27
|
+
return '1' if column.type == :integer
|
28
|
+
return 'any' if column.try(:scale).blank?
|
29
|
+
|
30
|
+
"0.#{'1'.rjust(column.scale, '0')}"
|
31
|
+
end
|
32
|
+
|
33
|
+
def min
|
34
|
+
return min_decimal if column.try(:precision).present?
|
35
|
+
return min_integer if column.try(:limit).present?
|
36
|
+
|
37
|
+
nil
|
38
|
+
end
|
39
|
+
|
40
|
+
def max
|
41
|
+
return max_decimal if column.try(:precision).present?
|
42
|
+
return max_integer if column.try(:limit).present?
|
43
|
+
|
44
|
+
nil
|
45
|
+
end
|
46
|
+
|
47
|
+
# XXX: This is the theoretical maximum value for a column with a given precision but,
|
48
|
+
# since the maximum database value is constrained by total significant figures (i.e.
|
49
|
+
# before and after the decimal point), an input can still be provided that would cause an
|
50
|
+
# error, so the default controller rescues `ActiveRecord::RangeError` to deal with this.
|
51
|
+
def max_decimal
|
52
|
+
'9' * column.precision
|
53
|
+
end
|
54
|
+
|
55
|
+
def min_decimal
|
56
|
+
"-#{'9' * column.precision}"
|
57
|
+
end
|
58
|
+
|
59
|
+
# `limit` represents available bytes for storing a signed integer e.g.
|
60
|
+
# 2**(8 * 8) / 2 - 1 == 9223372036854775807
|
61
|
+
# which matches PostgreSQL's `bigint` max value:
|
62
|
+
# https://www.postgresql.org/docs/current/datatype-numeric.html
|
63
|
+
def max_integer
|
64
|
+
((2**(column.limit * 8)) / 2) - 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def min_integer
|
68
|
+
-2**(column.limit * 8) / 2
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -9,6 +9,7 @@ require_relative 'util/form_value_mapping'
|
|
9
9
|
require_relative 'util/display_value_mapping'
|
10
10
|
require_relative 'util/association_mapping'
|
11
11
|
require_relative 'util/decorator'
|
12
|
+
require_relative 'util/numeric_field'
|
12
13
|
|
13
14
|
module ActiveElement
|
14
15
|
module Components
|
@@ -40,6 +41,13 @@ module ActiveElement
|
|
40
41
|
"#{namespace.classify}::#{base}".safe_constantize || base.safe_constantize
|
41
42
|
end
|
42
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
|
+
|
43
51
|
def self.json_pretty_print(json)
|
44
52
|
formatter = Rouge::Formatters::HTML.new
|
45
53
|
lexer = Rouge::Lexers::JSON.new
|