active_element 0.0.11 → 0.0.12
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 +1 -0
- data/Gemfile.lock +8 -1
- data/app/assets/javascripts/active_element/text_search_field.js +13 -1
- data/app/views/active_element/components/form/_label.html.erb +1 -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/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/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/text_search.rb +10 -1
- data/lib/active_element/components/util/form_field_mapping.rb +22 -10
- data/lib/active_element/components/util/numeric_field.rb +73 -0
- data/lib/active_element/components/util.rb +1 -0
- data/lib/active_element/controller_interface.rb +25 -29
- data/lib/active_element/controller_state.rb +44 -0
- data/lib/active_element/default_controller.rb +47 -3
- data/lib/active_element/default_record_params.rb +1 -1
- data/lib/active_element/{default_text_search.rb → default_search.rb} +6 -6
- data/lib/active_element/version.rb +1 -1
- data/lib/active_element.rb +2 -1
- metadata +7 -3
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: b461a50c1dc5d99ecb2a98633424871c2ea25c99817cb0f269b0e55da9521732
|
4
|
+
data.tar.gz: 2d27482a5ffb5ea966c3f15dfa0e412ddb61b04cb63dbd628d272e95072d89c4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d671afa4e3c6cea6eba9d3882bfbc3c21e2df90237d1afcd7c5c303fee433e8ce0a2a813402eeb79431ca2a33fcb737ed0879a0f3349b25ccba0b1b0763ec55b
|
7
|
+
data.tar.gz: 8e40f9498e58fd5430fa56290a5034c960dc5be81aa8b8e9e88fb790c138c7039a522e909a126bb6769d7cf680ce8937dfefa0e3c5942e5b1d541ac5b718d6ad
|
data/.rubocop.yml
CHANGED
data/Gemfile
CHANGED
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
active_element (0.0.
|
4
|
+
active_element (0.0.12)
|
5
5
|
bootstrap (~> 5.3.0alpha3)
|
6
6
|
kaminari (~> 1.2)
|
7
7
|
paintbrush (~> 0.1.2)
|
@@ -135,6 +135,9 @@ GEM
|
|
135
135
|
rexml
|
136
136
|
kramdown-parser-gfm (1.1.0)
|
137
137
|
kramdown (~> 2.0)
|
138
|
+
listen (3.8.0)
|
139
|
+
rb-fsevent (~> 0.10, >= 0.10.3)
|
140
|
+
rb-inotify (~> 0.9, >= 0.9.10)
|
138
141
|
loofah (2.21.3)
|
139
142
|
crass (~> 1.0.2)
|
140
143
|
nokogiri (>= 1.12.0)
|
@@ -203,6 +206,9 @@ GEM
|
|
203
206
|
zeitwerk (~> 2.5)
|
204
207
|
rainbow (3.1.1)
|
205
208
|
rake (13.0.6)
|
209
|
+
rb-fsevent (0.11.2)
|
210
|
+
rb-inotify (0.10.1)
|
211
|
+
ffi (~> 1.0)
|
206
212
|
redcarpet (3.6.0)
|
207
213
|
regexp_parser (2.8.1)
|
208
214
|
responders (3.1.0)
|
@@ -325,6 +331,7 @@ DEPENDENCIES
|
|
325
331
|
devpack (~> 0.4.1)
|
326
332
|
factory_bot_rails (~> 5.2)
|
327
333
|
faker (~> 2.23)
|
334
|
+
listen (~> 3.8)
|
328
335
|
rake (~> 13.0)
|
329
336
|
rspec (~> 3.12)
|
330
337
|
rspec-documentation (~> 0.0.9)
|
@@ -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
|
});
|
@@ -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 %>
|
@@ -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',
|
@@ -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
|
|
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
|
@@ -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
|
@@ -127,21 +127,21 @@ 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
|
-
Util.relation_controller(model, controller, field)
|
142
|
-
|
143
|
-
|
144
|
-
|
139
|
+
(Util.relation_controller(model, controller, field)&.active_element&.state&.searchable_fields || [])
|
140
|
+
.reject { |field| field.to_s.end_with?('_at') } # FIXME: Select strings/numbers only.
|
141
|
+
end
|
142
|
+
|
143
|
+
def relation_primary_key(field)
|
144
|
+
relation(field).options.fetch(:primary_key) { relation_model.primary_key }
|
145
145
|
end
|
146
146
|
|
147
147
|
def default_type_from_column_type(field, column_type) # rubocop:disable Metrics/MethodLength
|
@@ -204,7 +204,9 @@ module ActiveElement
|
|
204
204
|
end
|
205
205
|
|
206
206
|
def default_options(field)
|
207
|
-
{
|
207
|
+
{
|
208
|
+
required: required?(field)
|
209
|
+
}.merge(field_options(field))
|
208
210
|
end
|
209
211
|
|
210
212
|
def required?(field)
|
@@ -215,6 +217,16 @@ module ActiveElement
|
|
215
217
|
validator.kind == :presence && validator.attributes.include?(field.to_sym)
|
216
218
|
end
|
217
219
|
end
|
220
|
+
|
221
|
+
def field_options(field)
|
222
|
+
return NumericField.new(field: field, column: column(field)).options if numeric?(field)
|
223
|
+
|
224
|
+
{}
|
225
|
+
end
|
226
|
+
|
227
|
+
def numeric?(field)
|
228
|
+
%i[float decimal integer].include?(column(field)&.type)
|
229
|
+
end
|
218
230
|
end
|
219
231
|
end
|
220
232
|
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
|
@@ -16,7 +16,7 @@ module ActiveElement
|
|
16
16
|
def initialize(controller_class, controller_instance = nil)
|
17
17
|
@controller_class = controller_class
|
18
18
|
@controller_instance = controller_instance
|
19
|
-
initialize_state
|
19
|
+
initialize_state(controller_class)
|
20
20
|
@missing_template_store = {}
|
21
21
|
@authorize = false
|
22
22
|
end
|
@@ -26,19 +26,23 @@ module ActiveElement
|
|
26
26
|
end
|
27
27
|
|
28
28
|
def listable_fields(*args)
|
29
|
-
state
|
29
|
+
state.listable_fields.concat(args.map(&:to_sym)).uniq!
|
30
30
|
end
|
31
31
|
|
32
32
|
def viewable_fields(*args)
|
33
|
-
state
|
33
|
+
state.viewable_fields.concat(args.map(&:to_sym)).uniq!
|
34
34
|
end
|
35
35
|
|
36
36
|
def editable_fields(*args)
|
37
|
-
state
|
37
|
+
state.editable_fields.concat(args.map(&:to_sym)).uniq!
|
38
38
|
end
|
39
39
|
|
40
40
|
def searchable_fields(*args)
|
41
|
-
state
|
41
|
+
state.searchable_fields.concat(args.map(&:to_sym)).uniq!
|
42
|
+
end
|
43
|
+
|
44
|
+
def deletable
|
45
|
+
state.deletable = true
|
42
46
|
end
|
43
47
|
|
44
48
|
def application_name
|
@@ -46,43 +50,39 @@ module ActiveElement
|
|
46
50
|
end
|
47
51
|
|
48
52
|
def authenticate_with(&block)
|
49
|
-
state
|
53
|
+
state.authenticator = block
|
50
54
|
end
|
51
55
|
|
52
56
|
def authorize_with(&block)
|
53
57
|
@authorize = true
|
54
|
-
state
|
58
|
+
state.authorizor = block
|
55
59
|
end
|
56
60
|
|
57
61
|
def sign_out_with(method: :get, &block)
|
58
|
-
state
|
59
|
-
state
|
62
|
+
state.sign_out_method = method
|
63
|
+
state.sign_out_path = block
|
60
64
|
end
|
61
65
|
|
62
66
|
def sign_out_path
|
63
|
-
state
|
67
|
+
state.sign_out_path&.call
|
64
68
|
end
|
65
69
|
|
66
|
-
|
67
|
-
state[:sign_out_method]
|
68
|
-
end
|
70
|
+
delegate :sign_out_method, to: :state
|
69
71
|
|
70
72
|
def sign_in_with(method: :get, &block)
|
71
|
-
state
|
72
|
-
state
|
73
|
+
state.sign_in_method = method
|
74
|
+
state.sign_in_path = block
|
73
75
|
end
|
74
76
|
|
75
77
|
def sign_in_path
|
76
|
-
state
|
78
|
+
state.sign_in_path&.call
|
77
79
|
end
|
78
80
|
|
79
|
-
|
80
|
-
state[:sign_in_method]
|
81
|
-
end
|
81
|
+
delegate :sign_in_method, to: :state
|
82
82
|
|
83
83
|
def authenticate
|
84
84
|
authenticator&.call
|
85
|
-
@current_user = state
|
85
|
+
@current_user = state.authorizor&.call
|
86
86
|
|
87
87
|
nil
|
88
88
|
end
|
@@ -91,16 +91,12 @@ module ActiveElement
|
|
91
91
|
raise ArgumentError, "Must specify `with: '<permission>'` or `always: true`" unless with.present? || always
|
92
92
|
raise ArgumentError, 'Cannot specify both `with` and `always: true`' if with.present? && always
|
93
93
|
|
94
|
-
state
|
94
|
+
state.permissions << { with: with, always: always, action: action }
|
95
95
|
end
|
96
96
|
|
97
|
-
|
98
|
-
state[:authenticator]
|
99
|
-
end
|
97
|
+
delegate :authenticator, to: :state
|
100
98
|
|
101
|
-
|
102
|
-
state.fetch(:permissions)
|
103
|
-
end
|
99
|
+
delegate :permissions, to: :state
|
104
100
|
|
105
101
|
def component
|
106
102
|
return (@component ||= ActiveElement::Component.new(controller_instance)) unless controller_instance.nil?
|
@@ -116,8 +112,8 @@ module ActiveElement
|
|
116
112
|
|
117
113
|
attr_reader :controller_class, :controller_instance
|
118
114
|
|
119
|
-
def initialize_state
|
120
|
-
self.class.state[
|
115
|
+
def initialize_state(key)
|
116
|
+
self.class.state[key] ||= ControllerState.new(controller: controller_instance)
|
121
117
|
end
|
122
118
|
end
|
123
119
|
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveElement
|
4
|
+
# Stores various data for a controller, including various field definitions and authentication
|
5
|
+
# configuration. Used throughout ActiveElement for generating dynamic content based on
|
6
|
+
# controller configuration.
|
7
|
+
class ControllerState
|
8
|
+
attr_reader :permissions, :listable_fields, :viewable_fields, :editable_fields, :searchable_fields
|
9
|
+
attr_accessor :sign_in_path, :sign_in, :sign_in_method, :sign_out_path, :sign_out_method,
|
10
|
+
:deletable, :authorizor, :authenticator
|
11
|
+
|
12
|
+
def initialize(controller:)
|
13
|
+
@controller = controller
|
14
|
+
@permissions = []
|
15
|
+
@authenticator = nil
|
16
|
+
@authorizor = nil
|
17
|
+
@deletable = false
|
18
|
+
@listable_fields = []
|
19
|
+
@viewable_fields = []
|
20
|
+
@editable_fields = []
|
21
|
+
@searchable_fields = []
|
22
|
+
end
|
23
|
+
|
24
|
+
def deletable?
|
25
|
+
!!deletable
|
26
|
+
end
|
27
|
+
|
28
|
+
def viewable?
|
29
|
+
viewable_fields.present? || controller.public_methods(false).include?(:show)
|
30
|
+
end
|
31
|
+
|
32
|
+
def editable?
|
33
|
+
editable_fields.present? || controller.public_methods(false).include?(:edit)
|
34
|
+
end
|
35
|
+
|
36
|
+
def creatable?
|
37
|
+
editable_fields.present? || controller.public_methods(false).include?(:new)
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
attr_reader :controller
|
43
|
+
end
|
44
|
+
end
|
@@ -9,6 +9,8 @@ module ActiveElement
|
|
9
9
|
end
|
10
10
|
|
11
11
|
def index
|
12
|
+
return render_forbidden(:listable) unless configured?(:listable)
|
13
|
+
|
12
14
|
controller.render 'active_element/default_views/index',
|
13
15
|
locals: {
|
14
16
|
collection: collection,
|
@@ -17,16 +19,22 @@ module ActiveElement
|
|
17
19
|
end
|
18
20
|
|
19
21
|
def show
|
22
|
+
return render_forbidden(:viewable) unless configured?(:viewable)
|
23
|
+
|
20
24
|
controller.render 'active_element/default_views/show', locals: { record: record }
|
21
25
|
end
|
22
26
|
|
23
27
|
def new
|
28
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
29
|
+
|
24
30
|
controller.render 'active_element/default_views/new', locals: { record: model.new, namespace: namespace }
|
25
31
|
end
|
26
32
|
|
27
|
-
def create # rubocop:disable Metrics/AbcSize
|
33
|
+
def create # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
|
34
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
35
|
+
|
28
36
|
new_record = model.new(default_record_params.params)
|
29
|
-
# Ensure associations are applied
|
37
|
+
# XXX: Ensure associations are applied - there must be a better way.
|
30
38
|
if new_record.save && new_record.reload.update(default_record_params.params)
|
31
39
|
controller.flash.notice = "#{new_record.model_name.to_s.titleize} created successfully."
|
32
40
|
controller.redirect_to record_path(new_record, :show).path
|
@@ -34,13 +42,19 @@ module ActiveElement
|
|
34
42
|
controller.flash.now.alert = "Failed to create #{model.name.to_s.titleize}."
|
35
43
|
controller.render 'active_element/default_views/new', locals: { record: new_record, namespace: namespace }
|
36
44
|
end
|
45
|
+
rescue ActiveRecord::RangeError => e
|
46
|
+
render_range_error(error: e, action: :new)
|
37
47
|
end
|
38
48
|
|
39
49
|
def edit
|
50
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
51
|
+
|
40
52
|
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
41
53
|
end
|
42
54
|
|
43
55
|
def update # rubocop:disable Metrics/AbcSize
|
56
|
+
return render_forbidden(:editable) unless configured?(:editable)
|
57
|
+
|
44
58
|
if record.update(default_record_params.params)
|
45
59
|
controller.flash.notice = "#{record.model_name.to_s.titleize} updated successfully."
|
46
60
|
controller.redirect_to record_path(record, :show).path
|
@@ -48,9 +62,13 @@ module ActiveElement
|
|
48
62
|
controller.flash.now.alert = "Failed to update #{model.name.to_s.titleize}."
|
49
63
|
controller.render 'active_element/default_views/edit', locals: { record: record, namespace: namespace }
|
50
64
|
end
|
65
|
+
rescue ActiveRecord::RangeError => e
|
66
|
+
render_range_error(error: e, action: :edit)
|
51
67
|
end
|
52
68
|
|
53
69
|
def destroy
|
70
|
+
return render_forbidden(:deletable) unless configured?(:deletable)
|
71
|
+
|
54
72
|
record.destroy
|
55
73
|
controller.flash.notice = "Deleted #{record.model_name.to_s.titleize}."
|
56
74
|
controller.redirect_to record_path(model, :index).path
|
@@ -60,12 +78,26 @@ module ActiveElement
|
|
60
78
|
|
61
79
|
attr_reader :controller
|
62
80
|
|
81
|
+
def render_forbidden(type)
|
82
|
+
controller.render 'active_element/default_views/forbidden', locals: { type: type }
|
83
|
+
end
|
84
|
+
|
85
|
+
def configured?(type)
|
86
|
+
return state.deletable? if type == :deletable
|
87
|
+
|
88
|
+
state.public_send("#{type}_fields").present?
|
89
|
+
end
|
90
|
+
|
91
|
+
def state
|
92
|
+
@state ||= controller.active_element.state
|
93
|
+
end
|
94
|
+
|
63
95
|
def default_record_params
|
64
96
|
@default_record_params ||= ActiveElement::DefaultRecordParams.new(controller: controller, model: model)
|
65
97
|
end
|
66
98
|
|
67
99
|
def default_text_search
|
68
|
-
@default_text_search ||= ActiveElement::
|
100
|
+
@default_text_search ||= ActiveElement::DefaultSearch.new(controller: controller, model: model)
|
69
101
|
end
|
70
102
|
|
71
103
|
def record_path(record, type = nil)
|
@@ -89,5 +121,17 @@ module ActiveElement
|
|
89
121
|
|
90
122
|
model.left_outer_joins(default_text_search.search_relations).where(*default_text_search.text_search)
|
91
123
|
end
|
124
|
+
|
125
|
+
def render_range_error(error:, action:)
|
126
|
+
controller.flash.now.alert = formatted_error(error)
|
127
|
+
controller.render "active_element/default_views/#{action}", locals: { record: record, namespace: namespace }
|
128
|
+
end
|
129
|
+
|
130
|
+
def formatted_error(error)
|
131
|
+
return error.cause.message.split("\n").join(', ') if error.try(:cause)&.try(:message).present?
|
132
|
+
return error.message if error.try(:message).present?
|
133
|
+
|
134
|
+
I18n.t('active_element.unexpected_error')
|
135
|
+
end
|
92
136
|
end
|
93
137
|
end
|
@@ -12,7 +12,7 @@ module ActiveElement
|
|
12
12
|
def params
|
13
13
|
with_transformed_relations(
|
14
14
|
controller.params.require(controller.controller_name.singularize)
|
15
|
-
.permit(controller.active_element.state.
|
15
|
+
.permit(controller.active_element.state.editable_fields)
|
16
16
|
)
|
17
17
|
end
|
18
18
|
|
@@ -1,9 +1,10 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
module ActiveElement
|
4
|
-
# Full text search querying for DefaultController, provides full text search
|
5
|
-
# controllers with configured searchable fields. Includes support for querying
|
6
|
-
|
4
|
+
# Full text search and datetime querying for DefaultController, provides full text search
|
5
|
+
# filters for all controllers with configured searchable fields. Includes support for querying
|
6
|
+
# across relations.
|
7
|
+
class DefaultSearch
|
7
8
|
def initialize(controller:, model:)
|
8
9
|
@controller = controller
|
9
10
|
@model = model
|
@@ -40,8 +41,7 @@ module ActiveElement
|
|
40
41
|
attr_reader :controller, :model
|
41
42
|
|
42
43
|
def searchable_fields
|
43
|
-
|
44
|
-
base_fields.map do |field|
|
44
|
+
controller.active_element.state.searchable_fields.map do |field|
|
45
45
|
next field unless field.to_s.end_with?('_at')
|
46
46
|
|
47
47
|
{ field => %i[from to] }
|
@@ -66,7 +66,7 @@ module ActiveElement
|
|
66
66
|
value[:from].present? ? Time.zone.parse(value[:from]) + timezone_offset : -Float::INFINITY
|
67
67
|
end
|
68
68
|
|
69
|
-
def range_end
|
69
|
+
def range_end(value)
|
70
70
|
value[:to].present? ? Time.zone.parse(value[:to]) + timezone_offset : Float::INFINITY
|
71
71
|
end
|
72
72
|
|
data/lib/active_element.rb
CHANGED
@@ -12,10 +12,11 @@ require_relative 'active_element/active_menu_link'
|
|
12
12
|
require_relative 'active_element/permissions_check'
|
13
13
|
require_relative 'active_element/permissions_report'
|
14
14
|
require_relative 'active_element/controller_interface'
|
15
|
+
require_relative 'active_element/controller_state'
|
15
16
|
require_relative 'active_element/controller_action'
|
16
17
|
require_relative 'active_element/default_controller'
|
17
18
|
require_relative 'active_element/default_record_params'
|
18
|
-
require_relative 'active_element/
|
19
|
+
require_relative 'active_element/default_search'
|
19
20
|
require_relative 'active_element/pre_render_processors'
|
20
21
|
require_relative 'active_element/rails_component'
|
21
22
|
require_relative 'active_element/route'
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: active_element
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0.
|
4
|
+
version: 0.0.12
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Bob Farrell
|
8
8
|
autorequire:
|
9
9
|
bindir: exe
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-06-
|
11
|
+
date: 2023-06-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bootstrap
|
@@ -168,6 +168,7 @@ files:
|
|
168
168
|
- app/views/active_element/decorators/_datetime.html.erb
|
169
169
|
- app/views/active_element/decorators/_time.html.erb
|
170
170
|
- app/views/active_element/default_views/edit.html.erb
|
171
|
+
- app/views/active_element/default_views/forbidden.html.erb
|
171
172
|
- app/views/active_element/default_views/index.html.erb
|
172
173
|
- app/views/active_element/default_views/new.html.erb
|
173
174
|
- app/views/active_element/default_views/show.html.erb
|
@@ -186,6 +187,7 @@ files:
|
|
186
187
|
- app/views/layouts/active_element.html.erb
|
187
188
|
- app/views/layouts/active_element_error.html.erb
|
188
189
|
- config/brakeman.ignore
|
190
|
+
- config/locales/en.yml
|
189
191
|
- config/routes.rb
|
190
192
|
- example_app/.gitattributes
|
191
193
|
- example_app/.gitignore
|
@@ -309,13 +311,15 @@ files:
|
|
309
311
|
- lib/active_element/components/util/form_field_mapping.rb
|
310
312
|
- lib/active_element/components/util/form_value_mapping.rb
|
311
313
|
- lib/active_element/components/util/i18n.rb
|
314
|
+
- lib/active_element/components/util/numeric_field.rb
|
312
315
|
- lib/active_element/components/util/record_mapping.rb
|
313
316
|
- lib/active_element/components/util/record_path.rb
|
314
317
|
- lib/active_element/controller_action.rb
|
315
318
|
- lib/active_element/controller_interface.rb
|
319
|
+
- lib/active_element/controller_state.rb
|
316
320
|
- lib/active_element/default_controller.rb
|
317
321
|
- lib/active_element/default_record_params.rb
|
318
|
-
- lib/active_element/
|
322
|
+
- lib/active_element/default_search.rb
|
319
323
|
- lib/active_element/engine.rb
|
320
324
|
- lib/active_element/json_field_schema.rb
|
321
325
|
- lib/active_element/permissions_check.rb
|