active_element 0.0.11 → 0.0.12
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 +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
|