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
@@ -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
|