avo 1.22.1.pre.1 → 1.22.1.pre.2
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of avo might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/Gemfile.lock +6 -4
- data/app/assets/builds/avo.js +21 -16
- data/app/assets/builds/avo.js.map +2 -2
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +19 -19
- data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +10 -9
- data/app/components/avo/fields/select_field/edit_component.html.erb +3 -3
- data/app/controllers/avo/base_controller.rb +1 -0
- data/app/controllers/avo/relations_controller.rb +1 -4
- data/app/javascript/avo.js +1 -0
- data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +20 -22
- data/app/views/avo/partials/_turbo_frame_wrap.html.erb +1 -1
- data/app/views/avo/relations/new.html.erb +1 -1
- data/db/factories.rb +3 -3
- data/lib/avo/fields/base_field.rb +7 -4
- data/lib/avo/fields/belongs_to_field.rb +92 -11
- data/lib/avo/version.rb +1 -1
- data/public/avo-assets/avo.js +21 -16
- data/public/avo-assets/avo.js.map +2 -2
- metadata +2 -2
@@ -13,35 +13,35 @@ class Avo::Fields::BelongsToField::AutocompleteComponent < ViewComponent::Base
|
|
13
13
|
end
|
14
14
|
|
15
15
|
def field_label
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
@field.value&.class == @type ? @field.field_label : nil
|
22
|
-
end
|
23
|
-
else
|
24
|
-
@field.field_label
|
16
|
+
result = @field.field_label
|
17
|
+
|
18
|
+
# New records won't have the value (instantiated model) present but the polymorphic_type and polymorphic_id prefilled
|
19
|
+
if should_prefill?
|
20
|
+
result = @field.value&.class == @type ? @field.field_label : nil
|
25
21
|
end
|
22
|
+
|
23
|
+
result
|
26
24
|
end
|
27
25
|
|
28
26
|
def field_value
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
@field.value&.class == @type ? @field.field_value : nil
|
35
|
-
end
|
36
|
-
else
|
37
|
-
@field.field_value
|
27
|
+
result = @field.field_value
|
28
|
+
|
29
|
+
# New records won't have the value (instantiated model) present but the polymorphic_type and polymorphic_id prefilled
|
30
|
+
if should_prefill?
|
31
|
+
result = @field.value&.class == @type ? @field.field_value : nil
|
38
32
|
end
|
33
|
+
|
34
|
+
result
|
39
35
|
end
|
40
36
|
|
41
37
|
private
|
42
38
|
|
39
|
+
def should_prefill?
|
40
|
+
@field.is_polymorphic? && searchable? && !(new_record? && has_polymorphic_association?)
|
41
|
+
end
|
42
|
+
|
43
43
|
def searchable?
|
44
|
-
@
|
44
|
+
@field.searchable
|
45
45
|
end
|
46
46
|
|
47
47
|
def new_record?
|
@@ -13,7 +13,7 @@
|
|
13
13
|
data-association-class="<%= @field&.target_resource&.model_class || nil %>"
|
14
14
|
>
|
15
15
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
16
|
-
<%= @form.select
|
16
|
+
<%= @form.select @field.type_input_foreign_key, @field.types.map { |type| [type.to_s.underscore.humanize, type.to_s] },
|
17
17
|
{
|
18
18
|
value: @field.value,
|
19
19
|
include_blank: @field.placeholder,
|
@@ -29,7 +29,7 @@
|
|
29
29
|
# If the select field is disabled, no value will be sent. It's how HTML works.
|
30
30
|
# Thus the extra hidden field to actually send the related id to the server.
|
31
31
|
if disabled %>
|
32
|
-
<%= @form.hidden_field
|
32
|
+
<%= @form.hidden_field @field.type_input_foreign_key %>
|
33
33
|
<% end %>
|
34
34
|
<% end %>
|
35
35
|
<% @field.types.each do |type| %>
|
@@ -43,15 +43,16 @@
|
|
43
43
|
field: @field,
|
44
44
|
type: type,
|
45
45
|
model_key: model_keys[type.to_s],
|
46
|
-
foreign_key:
|
46
|
+
foreign_key: @field.id_input_foreign_key,
|
47
47
|
resource: @resource,
|
48
48
|
disabled: disabled,
|
49
49
|
polymorphic_record: polymorphic_record
|
50
50
|
%>
|
51
51
|
<% else %>
|
52
|
-
<%= @form.select
|
52
|
+
<%= @form.select @field.id_input_foreign_key,
|
53
|
+
options_for_select(@field.values_for_type(type), @resource.present? && @resource.model.present? ? @resource.model[@field.id_input_foreign_key] : nil),
|
53
54
|
{
|
54
|
-
value: @resource.model[
|
55
|
+
value: @resource.model[@field.id_input_foreign_key].to_s,
|
55
56
|
include_blank: @field.placeholder,
|
56
57
|
},
|
57
58
|
{
|
@@ -63,7 +64,7 @@
|
|
63
64
|
# If the select field is disabled, no value will be sent. It's how HTML works.
|
64
65
|
# Thus the extra hidden field to actually send the related id to the server.
|
65
66
|
if disabled %>
|
66
|
-
<%= @form.hidden_field
|
67
|
+
<%= @form.hidden_field @field.id_input_foreign_key %>
|
67
68
|
<% end %>
|
68
69
|
<% end %>
|
69
70
|
<% end %>
|
@@ -76,12 +77,12 @@
|
|
76
77
|
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form,
|
77
78
|
field: @field,
|
78
79
|
model_key: @field.target_resource&.model_key,
|
79
|
-
foreign_key: @field.
|
80
|
+
foreign_key: @field.id_input_foreign_key,
|
80
81
|
resource: @resource,
|
81
82
|
disabled: disabled
|
82
83
|
%>
|
83
84
|
<% else %>
|
84
|
-
<%= @form.select @field.
|
85
|
+
<%= @form.select @field.id_input_foreign_key, @field.options,
|
85
86
|
{
|
86
87
|
include_blank: @field.placeholder,
|
87
88
|
value: @field.value
|
@@ -95,7 +96,7 @@
|
|
95
96
|
# If the select field is disabled, no value will be sent. It's how HTML works.
|
96
97
|
# Thus the extra hidden field to actually send the related id to the server.
|
97
98
|
if disabled %>
|
98
|
-
<%= @form.hidden_field @field.
|
99
|
+
<%= @form.hidden_field @field.id_input_foreign_key %>
|
99
100
|
<% end %>
|
100
101
|
<% end %>
|
101
102
|
<% end %>
|
@@ -1,6 +1,6 @@
|
|
1
1
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
2
2
|
<%= @form.select @field.id, @field.options_for_select, { selected: @field.value, prompt: @field.placeholder }, {
|
3
|
-
|
4
|
-
|
5
|
-
|
3
|
+
class: helpers.input_classes(' w-full', has_error: @field.model_errors.include?(@field.id)),
|
4
|
+
disabled: @field.readonly,
|
5
|
+
value: @field.model.present? ? @field.model[@field.id] : @field.value } %>
|
6
6
|
<% end %>
|
@@ -84,6 +84,7 @@ module Avo
|
|
84
84
|
def new
|
85
85
|
@model = @resource.model_class.new
|
86
86
|
@resource = @resource.hydrate(model: @model, view: :new, user: _current_user)
|
87
|
+
# abort @model.course.inspect
|
87
88
|
|
88
89
|
@page_title = @resource.default_panel_name
|
89
90
|
add_breadcrumb resource_name.humanize, resources_path(resource: @resource)
|
@@ -37,10 +37,7 @@ module Avo
|
|
37
37
|
query = @authorization.apply_policy @attachment_class
|
38
38
|
|
39
39
|
@options = query.all.map do |model|
|
40
|
-
|
41
|
-
value: model.id,
|
42
|
-
label: model.send(@attachment_resource.class.title)
|
43
|
-
}
|
40
|
+
[model.send(@attachment_resource.class.title), model.id]
|
44
41
|
end
|
45
42
|
end
|
46
43
|
|
data/app/javascript/avo.js
CHANGED
@@ -59,6 +59,7 @@ document.addEventListener('turbo:before-fetch-response', (e) => {
|
|
59
59
|
})
|
60
60
|
document.addEventListener('turbo:visit', () => document.body.classList.add('turbo-loading'))
|
61
61
|
document.addEventListener('turbo:submit-start', () => document.body.classList.add('turbo-loading'))
|
62
|
+
document.addEventListener('turbo:submit-end', () => document.body.classList.remove('turbo-loading'))
|
62
63
|
document.addEventListener('turbo:before-cache', () => {
|
63
64
|
document.querySelectorAll('[data-turbo-remove-before-cache]').forEach((element) => element.remove())
|
64
65
|
})
|
@@ -58,29 +58,16 @@ export default class extends Controller {
|
|
58
58
|
|
59
59
|
if (this.isSearchable) {
|
60
60
|
const textInput = target.querySelector('input[type="text"]')
|
61
|
-
if (textInput)
|
62
|
-
textInput.setAttribute('valid-name', textInput.getAttribute('name'))
|
63
|
-
}
|
61
|
+
if (textInput) this.nameToValidName(textInput)
|
64
62
|
|
65
63
|
const hiddenInput = target.querySelector('input[type="hidden"]')
|
66
|
-
if (hiddenInput)
|
67
|
-
hiddenInput.setAttribute(
|
68
|
-
'valid-name',
|
69
|
-
hiddenInput.getAttribute('name'),
|
70
|
-
)
|
71
|
-
}
|
64
|
+
if (hiddenInput) this.nameToValidName(hiddenInput)
|
72
65
|
} else {
|
73
66
|
const select = target.querySelector('select')
|
74
|
-
if (select)
|
75
|
-
|
76
|
-
}
|
67
|
+
if (select) this.nameToValidName(select)
|
68
|
+
|
77
69
|
const hiddenInput = target.querySelector('input[type="hidden"]')
|
78
|
-
if (hiddenInput)
|
79
|
-
hiddenInput.setAttribute(
|
80
|
-
'valid-name',
|
81
|
-
hiddenInput.getAttribute('name'),
|
82
|
-
)
|
83
|
-
}
|
70
|
+
if (hiddenInput) this.nameToValidName(hiddenInput)
|
84
71
|
|
85
72
|
if (this.selectedType !== type) {
|
86
73
|
select.selectedIndex = 0
|
@@ -108,13 +95,16 @@ export default class extends Controller {
|
|
108
95
|
const textInput = target.querySelector('input[type="text"]')
|
109
96
|
const hiddenInput = target.querySelector('input[type="hidden"]')
|
110
97
|
|
111
|
-
|
112
|
-
|
98
|
+
this.validNameToName(textInput)
|
99
|
+
this.validNameToName(hiddenInput)
|
113
100
|
} else {
|
114
101
|
const select = target.querySelector('select')
|
115
102
|
const hiddenInput = target.querySelector('input[type="hidden"]')
|
116
|
-
|
117
|
-
|
103
|
+
this.validNameToName(select)
|
104
|
+
|
105
|
+
if (hiddenInput) {
|
106
|
+
this.validNameToName(hiddenInput)
|
107
|
+
}
|
118
108
|
}
|
119
109
|
}
|
120
110
|
|
@@ -135,4 +125,12 @@ export default class extends Controller {
|
|
135
125
|
} catch (error) {}
|
136
126
|
}
|
137
127
|
}
|
128
|
+
|
129
|
+
validNameToName(target) {
|
130
|
+
target.setAttribute('name', target.getAttribute('valid-name'))
|
131
|
+
}
|
132
|
+
|
133
|
+
nameToValidName(target) {
|
134
|
+
target.setAttribute('valid-name', target.getAttribute('name'))
|
135
|
+
}
|
138
136
|
}
|
@@ -3,7 +3,7 @@
|
|
3
3
|
# When rendering the frames the flashed content gets lost.
|
4
4
|
# By including the alerts partial, the stimulus will pick them up and display them to the user.
|
5
5
|
%>
|
6
|
-
<%= render partial: 'avo/partials/alerts'if flash.present?
|
6
|
+
<%= render partial: 'avo/partials/alerts' if flash.present? && name.present? %>
|
7
7
|
|
8
8
|
<%= yield %>
|
9
9
|
<% if name.present? %></turbo-frame><% end %>
|
@@ -11,7 +11,7 @@
|
|
11
11
|
|
12
12
|
<div class="flex-1 flex items-center justify-center px-8 text-lg mt-8 mb-12">
|
13
13
|
<div class="flex-1 flex flex-col items-center justify-center px-24 text-base">
|
14
|
-
<%= form.select :related_id, options_for_select(@options
|
14
|
+
<%= form.select :related_id, options_for_select(@options, nil),
|
15
15
|
{
|
16
16
|
include_blank: t('avo.choose_an_option'),
|
17
17
|
},
|
data/db/factories.rb
CHANGED
@@ -17,7 +17,7 @@ FactoryBot.define do
|
|
17
17
|
end
|
18
18
|
|
19
19
|
factory :post do
|
20
|
-
name { Faker::Quote.
|
20
|
+
name { Faker::Quote.famous_last_words }
|
21
21
|
body { Faker::Lorem.paragraphs(number: rand(4...10)).join("\n") }
|
22
22
|
is_featured { [true, false].sample }
|
23
23
|
published_at do
|
@@ -42,11 +42,11 @@ FactoryBot.define do
|
|
42
42
|
end
|
43
43
|
|
44
44
|
factory :comment do
|
45
|
-
body { Faker::Lorem.paragraphs(number: rand(4...10))
|
45
|
+
body { Faker::Lorem.paragraphs(number: rand(4...10)) }
|
46
46
|
end
|
47
47
|
|
48
48
|
factory :review do
|
49
|
-
body { Faker::Lorem.paragraphs(number: rand(4...10))
|
49
|
+
body { Faker::Lorem.paragraphs(number: rand(4...10)) }
|
50
50
|
end
|
51
51
|
|
52
52
|
factory :person do
|
@@ -128,11 +128,14 @@ module Avo
|
|
128
128
|
# Get model value
|
129
129
|
final_value = @model.send(property) if (model_or_class(@model) == "model") && @model.respond_to?(property)
|
130
130
|
|
131
|
+
# On new views and actions modals we need to prefill the fields
|
131
132
|
if (@view === :new) || @action.present?
|
132
|
-
|
133
|
-
default.call
|
134
|
-
|
135
|
-
|
133
|
+
if default.present?
|
134
|
+
final_value = if default.respond_to?(:call)
|
135
|
+
default.call
|
136
|
+
else
|
137
|
+
default
|
138
|
+
end
|
136
139
|
end
|
137
140
|
end
|
138
141
|
|
@@ -1,5 +1,62 @@
|
|
1
1
|
module Avo
|
2
2
|
module Fields
|
3
|
+
|
4
|
+
# The field can be in multiple scenarios where it needs different types of data and displays the state differently.
|
5
|
+
# For example the non-polymorphic, non-searchable variant is the easiest to support. You only need to populate a simple select with the ID of the associated record and the list of records.
|
6
|
+
# For the searchable polymorphic variant you need to provide the type of the association (Post, Project, Team), the label of the associated record ("Cool post title") and the ID of that record.
|
7
|
+
# Furthermore, the way Avo works, it needs to do some queries on the back-end to fetch the required information.
|
8
|
+
#
|
9
|
+
# Field scenarios:
|
10
|
+
# 1. Create new record
|
11
|
+
# List of records
|
12
|
+
# 2. Create new record as association
|
13
|
+
# List of records, the ID
|
14
|
+
# 3. Create new searchable record
|
15
|
+
# Nothing really. The records will be fetched from the search API
|
16
|
+
# 4. Create new searchable record as association
|
17
|
+
# The associated record label and ID. The records will be fetched from the search API
|
18
|
+
# 5. Create new polymorphic record
|
19
|
+
# Type & ID
|
20
|
+
# 6. Create new polymorphic record as association
|
21
|
+
# Type, list of records, and ID
|
22
|
+
# 7. Create new polymorphic searchable record
|
23
|
+
# Type, Label and ID
|
24
|
+
# 8. Create new polymorphic searchable record as association
|
25
|
+
# Type, Label and ID
|
26
|
+
# 9. Edit a record
|
27
|
+
# List of records & ID
|
28
|
+
# 10. Edit a record as searchable
|
29
|
+
# Label and ID
|
30
|
+
# 11. Edit a record as an association
|
31
|
+
# List and ID
|
32
|
+
# 12. Edit a record as an searchable association
|
33
|
+
# Label and ID
|
34
|
+
# 13. Edit a polymorphic record
|
35
|
+
# Type, List of records & ID
|
36
|
+
# 14. Edit a polymorphic record as searchable
|
37
|
+
# Type, Label and ID
|
38
|
+
# 15. Edit a polymorphic record as an association
|
39
|
+
# Type, List and ID
|
40
|
+
# 16. Edit a polymorphic record as an searchable association
|
41
|
+
# Type, Label and ID
|
42
|
+
# Also all of the above with a namespaced model `Course/Link`
|
43
|
+
|
44
|
+
# Variants
|
45
|
+
# 1. Select belongs to
|
46
|
+
# 2. Searchable belongs to
|
47
|
+
# 3. Select Polymorphic belongs to
|
48
|
+
# 4. Searchable Polymorphic belongs to
|
49
|
+
|
50
|
+
# Requirements
|
51
|
+
# - list
|
52
|
+
# - ID
|
53
|
+
# - label
|
54
|
+
# - Type
|
55
|
+
# - foreign_key
|
56
|
+
# - foreign_key for poly type
|
57
|
+
# - foreign_key for poly id
|
58
|
+
# - is_disabled?
|
59
|
+
|
3
60
|
class BelongsToField < BaseField
|
4
61
|
attr_reader :polymorphic_as
|
5
62
|
attr_reader :relation_method
|
@@ -21,7 +78,13 @@ module Avo
|
|
21
78
|
end
|
22
79
|
|
23
80
|
def value
|
24
|
-
|
81
|
+
if is_polymorphic?
|
82
|
+
# Get the value from the pre-filled assoociation record
|
83
|
+
super(polymorphic_as)
|
84
|
+
else
|
85
|
+
# Get the value from the pre-filled assoociation record
|
86
|
+
super(relation_method)
|
87
|
+
end
|
25
88
|
end
|
26
89
|
|
27
90
|
# The value
|
@@ -39,22 +102,40 @@ module Avo
|
|
39
102
|
end
|
40
103
|
|
41
104
|
def options
|
42
|
-
|
43
|
-
{
|
44
|
-
value: model.id,
|
45
|
-
label: model.send(target_resource.class.title)
|
46
|
-
}
|
47
|
-
end
|
105
|
+
values_for_type
|
48
106
|
end
|
49
107
|
|
50
|
-
def values_for_type(
|
51
|
-
|
52
|
-
|
108
|
+
def values_for_type(model = nil)
|
109
|
+
resource = target_resource
|
110
|
+
resource = App.get_resource_by_model_name model if model.present?
|
111
|
+
|
112
|
+
::Avo::Services::AuthorizationService.apply_policy(user, resource.class.query_scope).all.map do |model|
|
113
|
+
[model.send(resource.class.title), model.id]
|
53
114
|
end
|
54
115
|
end
|
55
116
|
|
56
117
|
def database_value
|
57
118
|
target_resource.id
|
119
|
+
rescue
|
120
|
+
nil
|
121
|
+
end
|
122
|
+
|
123
|
+
def type_input_foreign_key
|
124
|
+
if is_polymorphic?
|
125
|
+
"#{foreign_key}_type"
|
126
|
+
end
|
127
|
+
end
|
128
|
+
|
129
|
+
def id_input_foreign_key
|
130
|
+
if is_polymorphic?
|
131
|
+
"#{foreign_key}_id"
|
132
|
+
else
|
133
|
+
foreign_key
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def is_polymorphic?
|
138
|
+
polymorphic_as.present?
|
58
139
|
end
|
59
140
|
|
60
141
|
def foreign_key
|
@@ -118,7 +199,7 @@ module Avo
|
|
118
199
|
end
|
119
200
|
|
120
201
|
def target_resource
|
121
|
-
if
|
202
|
+
if is_polymorphic?
|
122
203
|
if value.present?
|
123
204
|
return App.get_resource_by_model_name(value.class)
|
124
205
|
else
|
data/lib/avo/version.rb
CHANGED
data/public/avo-assets/avo.js
CHANGED
@@ -81101,22 +81101,18 @@
|
|
81101
81101
|
const { type } = target.dataset;
|
81102
81102
|
if (this.isSearchable) {
|
81103
81103
|
const textInput = target.querySelector('input[type="text"]');
|
81104
|
-
if (textInput)
|
81105
|
-
|
81106
|
-
}
|
81104
|
+
if (textInput)
|
81105
|
+
this.nameToValidName(textInput);
|
81107
81106
|
const hiddenInput = target.querySelector('input[type="hidden"]');
|
81108
|
-
if (hiddenInput)
|
81109
|
-
|
81110
|
-
}
|
81107
|
+
if (hiddenInput)
|
81108
|
+
this.nameToValidName(hiddenInput);
|
81111
81109
|
} else {
|
81112
81110
|
const select = target.querySelector("select");
|
81113
|
-
if (select)
|
81114
|
-
|
81115
|
-
}
|
81111
|
+
if (select)
|
81112
|
+
this.nameToValidName(select);
|
81116
81113
|
const hiddenInput = target.querySelector('input[type="hidden"]');
|
81117
|
-
if (hiddenInput)
|
81118
|
-
|
81119
|
-
}
|
81114
|
+
if (hiddenInput)
|
81115
|
+
this.nameToValidName(hiddenInput);
|
81120
81116
|
if (this.selectedType !== type) {
|
81121
81117
|
select.selectedIndex = 0;
|
81122
81118
|
}
|
@@ -81134,13 +81130,15 @@
|
|
81134
81130
|
if (this.isSearchable) {
|
81135
81131
|
const textInput = target.querySelector('input[type="text"]');
|
81136
81132
|
const hiddenInput = target.querySelector('input[type="hidden"]');
|
81137
|
-
|
81138
|
-
|
81133
|
+
this.validNameToName(textInput);
|
81134
|
+
this.validNameToName(hiddenInput);
|
81139
81135
|
} else {
|
81140
81136
|
const select = target.querySelector("select");
|
81141
81137
|
const hiddenInput = target.querySelector('input[type="hidden"]');
|
81142
|
-
|
81143
|
-
|
81138
|
+
this.validNameToName(select);
|
81139
|
+
if (hiddenInput) {
|
81140
|
+
this.validNameToName(hiddenInput);
|
81141
|
+
}
|
81144
81142
|
}
|
81145
81143
|
}
|
81146
81144
|
invalidateTarget(target) {
|
@@ -81158,6 +81156,12 @@
|
|
81158
81156
|
}
|
81159
81157
|
}
|
81160
81158
|
}
|
81159
|
+
validNameToName(target) {
|
81160
|
+
target.setAttribute("name", target.getAttribute("valid-name"));
|
81161
|
+
}
|
81162
|
+
nameToValidName(target) {
|
81163
|
+
target.setAttribute("valid-name", target.getAttribute("name"));
|
81164
|
+
}
|
81161
81165
|
};
|
81162
81166
|
__publicField(belongs_to_field_controller_default, "targets", ["select", "type", "loadAssociationLink"]);
|
81163
81167
|
|
@@ -87796,6 +87800,7 @@
|
|
87796
87800
|
});
|
87797
87801
|
document.addEventListener("turbo:visit", () => document.body.classList.add("turbo-loading"));
|
87798
87802
|
document.addEventListener("turbo:submit-start", () => document.body.classList.add("turbo-loading"));
|
87803
|
+
document.addEventListener("turbo:submit-end", () => document.body.classList.remove("turbo-loading"));
|
87799
87804
|
document.addEventListener("turbo:before-cache", () => {
|
87800
87805
|
document.querySelectorAll("[data-turbo-remove-before-cache]").forEach((element) => element.remove());
|
87801
87806
|
});
|