avo 1.20.2.pre.2 → 1.21.0.pre.1
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 +1 -3
- data/Gemfile.lock +3 -5
- data/app/assets/builds/avo.css +0 -16
- data/app/assets/builds/avo.js +234 -317
- data/app/assets/builds/avo.js.map +2 -2
- data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +33 -39
- data/app/components/avo/views/resource_index_component.html.erb +1 -1
- data/app/controllers/avo/application_controller.rb +13 -11
- data/app/controllers/avo/base_controller.rb +4 -1
- data/app/controllers/avo/relations_controller.rb +4 -4
- data/app/controllers/avo/search_controller.rb +1 -0
- data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +33 -96
- data/app/javascript/js/controllers/search_controller.js +17 -83
- data/app/views/avo/partials/_global_search.html.erb +1 -0
- data/app/views/avo/partials/_javascript.html.erb +0 -1
- data/app/views/avo/partials/_resource_search.html.erb +1 -0
- data/db/factories.rb +0 -4
- data/lib/avo/fields/base_field.rb +1 -1
- data/lib/avo/fields/belongs_to_field.rb +2 -19
- data/lib/avo/fields/files_field.rb +1 -2
- data/lib/avo/licensing/pro_license.rb +1 -2
- data/lib/avo/version.rb +1 -1
- data/lib/generators/avo/templates/locales/avo.en.yml +0 -1
- data/public/avo-assets/avo.css +0 -16
- data/public/avo-assets/avo.js +234 -317
- data/public/avo-assets/avo.js.map +2 -2
- metadata +2 -5
- data/app/assets/svgs/x.svg +0 -3
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +0 -22
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +0 -33
@@ -1,34 +1,22 @@
|
|
1
|
-
<%
|
2
|
-
|
3
|
-
|
4
|
-
# Set the model keys so we can pass them over
|
5
|
-
model_keys = @field.types.map do |type|
|
6
|
-
resource = Avo::App.get_resource_by_model_name(type.to_s)
|
7
|
-
[type.to_s, resource.model_key]
|
8
|
-
end.to_h
|
9
|
-
%>
|
10
|
-
<div data-controller="belongs-to-field"
|
11
|
-
data-searchable="<%= @field.searchable %>"
|
12
|
-
data-association="<%= @field.id %>"
|
13
|
-
data-association-class="<%= @field&.target_resource&.model_class || nil %>"
|
14
|
-
>
|
1
|
+
<% if @field.types.present? %>
|
2
|
+
<div data-controller="belongs-to-field">
|
15
3
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
16
4
|
<%= @form.select "#{@field.foreign_key}_type", @field.types.map { |type| [type.to_s.underscore.humanize, type.to_s] },
|
17
5
|
{
|
18
|
-
value: @field.value,
|
19
6
|
include_blank: @field.placeholder,
|
20
7
|
},
|
21
8
|
{
|
22
9
|
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
23
10
|
disabled: disabled,
|
24
11
|
'data-belongs-to-field-target': "select",
|
25
|
-
'data-action': 'change->belongs-to-field#
|
12
|
+
'data-action': 'change->belongs-to-field#changedType'
|
26
13
|
}
|
27
14
|
%>
|
28
15
|
<%
|
29
16
|
# If the select field is disabled, no value will be sent. It's how HTML works.
|
30
17
|
# Thus the extra hidden field to actually send the related id to the server.
|
31
|
-
if disabled
|
18
|
+
if disabled
|
19
|
+
%>
|
32
20
|
<%= @form.hidden_field "#{@field.foreign_key}_type" %>
|
33
21
|
<% end %>
|
34
22
|
<% end %>
|
@@ -38,18 +26,21 @@
|
|
38
26
|
data-type="<%= type %>"
|
39
27
|
>
|
40
28
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal, label: type.to_s.underscore.humanize do %>
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
50
|
-
disabled: disabled
|
51
|
-
}
|
29
|
+
<%= @form.select "#{@field.foreign_key}_id", @field.values_for_type(type),
|
30
|
+
{
|
31
|
+
include_blank: @field.placeholder,
|
32
|
+
},
|
33
|
+
{
|
34
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
35
|
+
disabled: disabled
|
36
|
+
}
|
52
37
|
%>
|
38
|
+
<%
|
39
|
+
# If the select field is disabled, no value will be sent. It's how HTML works.
|
40
|
+
# Thus the extra hidden field to actually send the related id to the server.
|
41
|
+
if disabled
|
42
|
+
%>
|
43
|
+
<%= @form.hidden_field "#{@field.foreign_key}_id" %>
|
53
44
|
<% end %>
|
54
45
|
<% end %>
|
55
46
|
</div>
|
@@ -57,18 +48,21 @@
|
|
57
48
|
</div>
|
58
49
|
<% else %>
|
59
50
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
69
|
-
disabled: disabled
|
70
|
-
}
|
51
|
+
<%= @form.select @field.foreign_key, @field.options.map { |o| [o[:label], o[:value]] },
|
52
|
+
{
|
53
|
+
include_blank: @field.placeholder,
|
54
|
+
},
|
55
|
+
{
|
56
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
57
|
+
disabled: disabled
|
58
|
+
}
|
71
59
|
%>
|
60
|
+
<%
|
61
|
+
# If the select field is disabled, no value will be sent. It's how HTML works.
|
62
|
+
# Thus the extra hidden field to actually send the related id to the server.
|
63
|
+
if disabled
|
64
|
+
%>
|
65
|
+
<%= @form.hidden_field @field.foreign_key %>
|
72
66
|
<% end %>
|
73
67
|
<% end %>
|
74
68
|
<% end %>
|
@@ -24,7 +24,7 @@
|
|
24
24
|
data-selected-resources="[]"
|
25
25
|
>
|
26
26
|
<div class="flex items-center px-6 w-64">
|
27
|
-
<%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.
|
27
|
+
<%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.model_class.model_name.plural} if @resource.search_query.present? %>
|
28
28
|
</div>
|
29
29
|
<div class="flex justify-end items-center px-6 space-x-3">
|
30
30
|
<%= render partial: 'avo/partials/view_toggle_button', locals: { available_view_types: available_view_types, view_type: view_type, turbo_frame: @turbo_frame } if @models.present? %>
|
@@ -111,13 +111,7 @@ module Avo
|
|
111
111
|
end
|
112
112
|
|
113
113
|
def fill_model
|
114
|
-
|
115
|
-
is_attach_action = params[model_param_key].blank? && params[:related_name].present? && params[:fields].present?
|
116
|
-
# puts ['fill_model->', is_attach_action, model_param_key].inspect
|
117
|
-
|
118
|
-
unless is_attach_action
|
119
|
-
@model = @resource.fill_model(@model_to_fill, cast_nullable(model_params))
|
120
|
-
end
|
114
|
+
@model = @resource.fill_model(@model_to_fill, cast_nullable(model_params))
|
121
115
|
end
|
122
116
|
|
123
117
|
def hydrate_resource
|
@@ -193,6 +187,18 @@ module Avo
|
|
193
187
|
query
|
194
188
|
end
|
195
189
|
|
190
|
+
# def authorize_user
|
191
|
+
# return if params[:controller] == 'avo/search'
|
192
|
+
|
193
|
+
# model = record = resource.model
|
194
|
+
|
195
|
+
# if ['show', 'edit', 'update'].include?(params[:action]) && params[:controller] == 'avo/resources'
|
196
|
+
# record = resource
|
197
|
+
# end
|
198
|
+
|
199
|
+
# # AuthorizationService::authorize_action _current_user, record, params[:action] return render_unauthorized unless
|
200
|
+
# end
|
201
|
+
|
196
202
|
def _authenticate!
|
197
203
|
instance_eval(&Avo.configuration.authenticate)
|
198
204
|
end
|
@@ -237,9 +243,5 @@ module Avo
|
|
237
243
|
def on_api_path
|
238
244
|
request.original_url.match?(/.*#{Avo::App.root_path}\/avo_api\/.*/)
|
239
245
|
end
|
240
|
-
|
241
|
-
def model_param_key
|
242
|
-
@resource.form_scope
|
243
|
-
end
|
244
246
|
end
|
245
247
|
end
|
@@ -7,7 +7,6 @@ module Avo
|
|
7
7
|
before_action :hydrate_resource
|
8
8
|
before_action :set_model, only: [:show, :edit, :destroy, :update]
|
9
9
|
before_action :set_model_to_fill
|
10
|
-
before_action :fill_model, only: [:create, :update]
|
11
10
|
before_action :authorize_action
|
12
11
|
before_action :reset_pagination_if_filters_changed, only: :index
|
13
12
|
before_action :cache_applied_filters, only: :index
|
@@ -113,6 +112,7 @@ module Avo
|
|
113
112
|
|
114
113
|
def create
|
115
114
|
# model gets instantiated and filled in the fill_model method
|
115
|
+
fill_model
|
116
116
|
saved = @model.save
|
117
117
|
@resource.hydrate(model: @model, view: :new, user: _current_user)
|
118
118
|
|
@@ -166,6 +166,7 @@ module Avo
|
|
166
166
|
|
167
167
|
def update
|
168
168
|
# model gets instantiated and filled in the fill_model method
|
169
|
+
fill_model
|
169
170
|
saved = @model.save
|
170
171
|
@resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
|
171
172
|
|
@@ -193,6 +194,8 @@ module Avo
|
|
193
194
|
private
|
194
195
|
|
195
196
|
def model_params
|
197
|
+
model_param_key = @resource.form_scope
|
198
|
+
|
196
199
|
request_params = params.require(model_param_key).permit(permitted_params)
|
197
200
|
|
198
201
|
if @resource.devise_password_optional && request_params[:password].blank? && request_params[:password_confirmation].blank?
|
@@ -4,11 +4,11 @@ module Avo
|
|
4
4
|
class RelationsController < BaseController
|
5
5
|
before_action :set_model, only: [:show, :index, :new, :create, :destroy]
|
6
6
|
before_action :set_related_resource_name
|
7
|
-
before_action :set_related_resource
|
8
|
-
before_action :hydrate_related_resource
|
7
|
+
before_action :set_related_resource
|
8
|
+
before_action :hydrate_related_resource
|
9
9
|
before_action :set_related_model, only: [:show]
|
10
|
-
before_action :set_attachment_class
|
11
|
-
before_action :set_attachment_resource
|
10
|
+
before_action :set_attachment_class
|
11
|
+
before_action :set_attachment_resource
|
12
12
|
before_action :set_attachment_model, only: [:create, :destroy]
|
13
13
|
before_action :set_reflection, only: [:index, :show]
|
14
14
|
|
@@ -1,128 +1,65 @@
|
|
1
1
|
import { Controller } from 'stimulus'
|
2
2
|
|
3
3
|
export default class extends Controller {
|
4
|
-
static targets = ['select', 'type'
|
5
|
-
|
6
|
-
defaults = {};
|
4
|
+
static targets = ['select', 'type']
|
7
5
|
|
8
6
|
get selectedType() {
|
9
7
|
return this.selectTarget.value
|
10
8
|
}
|
11
9
|
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
get association() {
|
17
|
-
return this.context.scope.element.dataset.association
|
10
|
+
connect() {
|
11
|
+
this.setValidNames()
|
12
|
+
this.changedType()
|
18
13
|
}
|
19
14
|
|
20
|
-
|
21
|
-
|
22
|
-
|
15
|
+
setValidNames() {
|
16
|
+
this.typeTargets.forEach((target) => {
|
17
|
+
const { type } = target.dataset
|
18
|
+
const select = target.querySelector('select')
|
19
|
+
const name = select.getAttribute('name')
|
23
20
|
|
24
|
-
|
25
|
-
|
26
|
-
|
21
|
+
select.setAttribute('valid-name', name)
|
22
|
+
if (this.selectedType !== type) {
|
23
|
+
select.selectedIndex = 0
|
24
|
+
}
|
25
|
+
})
|
27
26
|
}
|
28
27
|
|
29
|
-
|
30
|
-
this.
|
31
|
-
this.
|
28
|
+
changedType() {
|
29
|
+
this.hideAllTypeTargets()
|
30
|
+
this.enableType(this.selectTarget.value)
|
32
31
|
}
|
33
32
|
|
34
|
-
|
35
|
-
|
36
|
-
hideAllTypes() {
|
33
|
+
hideAllTypeTargets() {
|
37
34
|
this.typeTargets.forEach((target) => {
|
38
|
-
|
39
|
-
|
35
|
+
this.hideTarget(target)
|
40
36
|
this.invalidateTarget(target)
|
41
37
|
})
|
42
38
|
}
|
43
39
|
|
44
|
-
|
45
|
-
|
46
|
-
* Ex: There are two types Article and Project and the Comment has commentable_id 3 and commentable_type: Article
|
47
|
-
* When you change the type from Project to Article the Project field will override the commentable_id value
|
48
|
-
* because it was rendered later (alphabetical sorting) and the browser will pick that one up.
|
49
|
-
* So we go and copy the name attribute to valid-name for all types and then copy it back to name when the user selects it.
|
50
|
-
*/
|
51
|
-
|
52
|
-
/**
|
53
|
-
* This method does the initial copying from name to valid-name.
|
54
|
-
*/
|
55
|
-
copyValidNames() {
|
56
|
-
this.typeTargets.forEach((target) => {
|
57
|
-
const { type } = target.dataset
|
58
|
-
|
59
|
-
if (this.isSearchable) {
|
60
|
-
const textInput = target.querySelector('input[type="text"]')
|
61
|
-
if (textInput) {
|
62
|
-
textInput.setAttribute('valid-name', textInput.getAttribute('name'))
|
63
|
-
}
|
64
|
-
|
65
|
-
const hiddenInput = target.querySelector('input[type="hidden"]')
|
66
|
-
if (hiddenInput) {
|
67
|
-
hiddenInput.setAttribute(
|
68
|
-
'valid-name',
|
69
|
-
hiddenInput.getAttribute('name'),
|
70
|
-
)
|
71
|
-
}
|
72
|
-
} else {
|
73
|
-
const select = target.querySelector('select')
|
74
|
-
if (select) {
|
75
|
-
select.setAttribute('valid-name', select.getAttribute('name'))
|
76
|
-
}
|
77
|
-
|
78
|
-
if (this.selectedType !== type) {
|
79
|
-
select.selectedIndex = 0
|
80
|
-
}
|
81
|
-
}
|
82
|
-
})
|
40
|
+
hideTarget(target) {
|
41
|
+
target.classList.add('hidden')
|
83
42
|
}
|
84
43
|
|
85
|
-
|
86
|
-
const
|
87
|
-
(typeTarget) => typeTarget.dataset.type === type,
|
88
|
-
)
|
89
|
-
if (target) {
|
90
|
-
target.classList.remove('hidden')
|
44
|
+
invalidateTarget(target) {
|
45
|
+
const select = target.querySelector('select')
|
91
46
|
|
92
|
-
|
93
|
-
}
|
47
|
+
select.setAttribute('name', '')
|
94
48
|
}
|
95
49
|
|
96
|
-
/**
|
97
|
-
* Copy value from `valid-name` to `name`
|
98
|
-
*/
|
99
50
|
validateTarget(target) {
|
100
|
-
|
101
|
-
|
102
|
-
const hiddenInput = target.querySelector('input[type="hidden"]')
|
51
|
+
const select = target.querySelector('select')
|
52
|
+
const validName = select.getAttribute('valid-name')
|
103
53
|
|
104
|
-
|
105
|
-
hiddenInput.setAttribute('name', hiddenInput.getAttribute('valid-name'))
|
106
|
-
} else {
|
107
|
-
const select = target.querySelector('select')
|
108
|
-
select.setAttribute('name', select.getAttribute('valid-name'))
|
109
|
-
}
|
54
|
+
select.setAttribute('name', validName)
|
110
55
|
}
|
111
56
|
|
112
|
-
|
113
|
-
|
114
|
-
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
try {
|
119
|
-
target.querySelector('input[type="text"]').setAttribute('name', '')
|
120
|
-
target.querySelector('input[type="hidden"]').setAttribute('name', '')
|
121
|
-
} catch {}
|
122
|
-
} else if (target) {
|
123
|
-
try {
|
124
|
-
target.querySelector('select').setAttribute('name', '')
|
125
|
-
} catch (error) {}
|
57
|
+
enableType(type) {
|
58
|
+
const target = this.typeTargets.find((typeTarget) => typeTarget.dataset.type === type)
|
59
|
+
|
60
|
+
if (target) {
|
61
|
+
target.classList.remove('hidden')
|
62
|
+
this.validateTarget(target)
|
126
63
|
}
|
127
64
|
}
|
128
65
|
}
|
@@ -3,40 +3,17 @@ import * as Mousetrap from 'mousetrap'
|
|
3
3
|
import { Controller } from 'stimulus'
|
4
4
|
import { Turbo } from '@hotwired/turbo-rails'
|
5
5
|
import { autocomplete } from '@algolia/autocomplete-js'
|
6
|
-
import URI from 'urijs'
|
7
6
|
import debouncePromise from '../helpers/debounce_promise'
|
8
7
|
|
9
|
-
/**
|
10
|
-
* The search controller is used in three places.
|
11
|
-
* 1. Global search (on the top navbar) and can search through multiple resources.
|
12
|
-
* 2. Resource search (on the Index page on top of the table panel) and will search one resource
|
13
|
-
* 3. belongs_to field. This requires a bit more cleanup because the user will not navigate away from the page.
|
14
|
-
* It will replace the id and label in some fields on the page and also needs a "clear" button which clears the information so the user can submit the form without a value.
|
15
|
-
*/
|
16
8
|
export default class extends Controller {
|
17
|
-
static targets = [
|
18
|
-
'autocomplete',
|
19
|
-
'button',
|
20
|
-
'hiddenId',
|
21
|
-
'visibleLabel',
|
22
|
-
'clearValue',
|
23
|
-
'clearButton',
|
24
|
-
];
|
25
|
-
|
26
|
-
debouncedFetch = debouncePromise(fetch, this.searchDebounce);
|
27
|
-
|
28
|
-
get dataset() {
|
29
|
-
return this.autocompleteTarget.dataset
|
30
|
-
}
|
9
|
+
static targets = ['autocomplete', 'button']
|
31
10
|
|
32
|
-
|
33
|
-
return window.Avo.configuration.search_debounce
|
34
|
-
}
|
11
|
+
debouncedFetch = debouncePromise(fetch, this.debounceTimeout)
|
35
12
|
|
36
13
|
get translationKeys() {
|
37
14
|
let keys
|
38
15
|
try {
|
39
|
-
keys = JSON.parse(this.dataset.translationKeys)
|
16
|
+
keys = JSON.parse(this.autocompleteTarget.dataset.translationKeys)
|
40
17
|
} catch (error) {
|
41
18
|
keys = {}
|
42
19
|
}
|
@@ -44,48 +21,20 @@ export default class extends Controller {
|
|
44
21
|
return keys
|
45
22
|
}
|
46
23
|
|
47
|
-
get
|
48
|
-
return this.dataset.
|
24
|
+
get debounceTimeout() {
|
25
|
+
return this.autocompleteTarget.dataset.debounceTimeout
|
49
26
|
}
|
50
27
|
|
51
|
-
get
|
52
|
-
return this.dataset.searchResource
|
28
|
+
get searchResource() {
|
29
|
+
return this.autocompleteTarget.dataset.searchResource
|
53
30
|
}
|
54
31
|
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
let params = { q: query }
|
59
|
-
let segments = [
|
60
|
-
window.Avo.configuration.root_path,
|
61
|
-
'avo_api',
|
62
|
-
this.dataset.searchResource,
|
63
|
-
'search',
|
64
|
-
]
|
65
|
-
|
66
|
-
if (this.isGlobalSearch) {
|
67
|
-
segments = [window.Avo.configuration.root_path, 'avo_api', 'search']
|
68
|
-
}
|
69
|
-
|
70
|
-
if (this.isBelongsToSearch) {
|
71
|
-
// eslint-disable-next-line camelcase
|
72
|
-
params = { ...params, via_association: this.dataset.viaAssociation }
|
73
|
-
}
|
74
|
-
|
75
|
-
return url.segment(segments).search(params).toString()
|
32
|
+
get isGlobalSearch() {
|
33
|
+
return this.searchResource === 'global'
|
76
34
|
}
|
77
35
|
|
78
|
-
|
79
|
-
|
80
|
-
this.hiddenIdTarget.setAttribute('value', item._id)
|
81
|
-
this.buttonTarget.setAttribute('value', item._label)
|
82
|
-
|
83
|
-
document.querySelector('.aa-DetachedOverlay').remove()
|
84
|
-
|
85
|
-
this.clearButtonTarget.classList.remove('hidden')
|
86
|
-
} else {
|
87
|
-
Turbo.visit(item._url, { action: 'advance' })
|
88
|
-
}
|
36
|
+
get searchUrl() {
|
37
|
+
return this.isGlobalSearch ? `${window.Avo.configuration.root_path}/avo_api/search` : `${window.Avo.configuration.root_path}/avo_api/${this.searchResource}/search`
|
89
38
|
}
|
90
39
|
|
91
40
|
addSource(resourceName, data) {
|
@@ -94,7 +43,9 @@ export default class extends Controller {
|
|
94
43
|
return {
|
95
44
|
sourceId: resourceName,
|
96
45
|
getItems: () => data.results,
|
97
|
-
onSelect
|
46
|
+
onSelect({ item }) {
|
47
|
+
Turbo.visit(item._url, { action: 'replace' })
|
48
|
+
},
|
98
49
|
templates: {
|
99
50
|
header() {
|
100
51
|
return `${data.header.toUpperCase()} ${data.help}`
|
@@ -133,10 +84,7 @@ export default class extends Controller {
|
|
133
84
|
})
|
134
85
|
},
|
135
86
|
noResults() {
|
136
|
-
return that.translationKeys.no_item_found.replace(
|
137
|
-
'%{item}',
|
138
|
-
resourceName,
|
139
|
-
)
|
87
|
+
return that.translationKeys.no_item_found.replace('%{item}', resourceName)
|
140
88
|
},
|
141
89
|
},
|
142
90
|
}
|
@@ -146,22 +94,11 @@ export default class extends Controller {
|
|
146
94
|
this.autocompleteTarget.querySelector('button').click()
|
147
95
|
}
|
148
96
|
|
149
|
-
clearValue() {
|
150
|
-
this.clearValueTargets.map((e) => e.setAttribute('value', ''))
|
151
|
-
this.clearButtonTarget.classList.add('hidden')
|
152
|
-
}
|
153
|
-
|
154
97
|
connect() {
|
155
98
|
const that = this
|
156
99
|
|
157
100
|
this.buttonTarget.onclick = () => this.showSearchPanel()
|
158
101
|
|
159
|
-
this.clearValueTargets.forEach((target) => {
|
160
|
-
if (target.getAttribute('value')) {
|
161
|
-
this.clearButtonTarget.classList.remove('hidden')
|
162
|
-
}
|
163
|
-
})
|
164
|
-
|
165
102
|
if (this.isGlobalSearch) {
|
166
103
|
Mousetrap.bind(['command+k', 'ctrl+k'], () => this.showSearchPanel())
|
167
104
|
}
|
@@ -175,15 +112,12 @@ export default class extends Controller {
|
|
175
112
|
openOnFocus: true,
|
176
113
|
detachedMediaQuery: '',
|
177
114
|
getSources: ({ query }) => {
|
178
|
-
const endpoint = that.searchUrl
|
115
|
+
const endpoint = `${that.searchUrl}?q=${query}`
|
179
116
|
|
180
|
-
return that
|
181
|
-
.debouncedFetch(endpoint)
|
117
|
+
return that.debouncedFetch(endpoint)
|
182
118
|
.then((response) => response.json())
|
183
119
|
.then((data) => Object.keys(data).map((resourceName) => that.addSource(resourceName, data[resourceName])))
|
184
120
|
},
|
185
121
|
})
|
186
|
-
|
187
|
-
this.buttonTarget.removeAttribute('disabled')
|
188
122
|
}
|
189
123
|
}
|
@@ -3,6 +3,7 @@
|
|
3
3
|
data-search-target="autocomplete"
|
4
4
|
data-search-resource="global"
|
5
5
|
data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>", "placeholder": "<%= I18n.translate 'avo.search.placeholder' %>", "cancel_button": "<%= I18n.translate 'avo.search.cancel_button' %>"}'
|
6
|
+
data-debounce-timeout='<%= Avo.configuration.search_debounce %>'
|
6
7
|
>
|
7
8
|
</div>
|
8
9
|
<div class="relative top-[-5px] inline-flex text-gray-400 text-sm leading-5 py-0.5 px-1.5 border border-gray-300 rounded-md cursor-pointer"
|
@@ -3,6 +3,7 @@
|
|
3
3
|
data-search-target="autocomplete"
|
4
4
|
data-search-resource="<%= resource %>"
|
5
5
|
data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>"}'
|
6
|
+
data-debounce-timeout='<%= Avo.configuration.search_debounce %>'
|
6
7
|
>
|
7
8
|
</div>
|
8
9
|
<div class="hidden relative inline-flex text-gray-400 text-sm border border-gray-300 rounded-full cursor-pointer" data-search-target="button"></div>
|
data/db/factories.rb
CHANGED
@@ -45,10 +45,6 @@ FactoryBot.define do
|
|
45
45
|
body { Faker::Lorem.paragraphs(number: rand(4...10)).join("\n") }
|
46
46
|
end
|
47
47
|
|
48
|
-
factory :review do
|
49
|
-
body { Faker::Lorem.paragraphs(number: rand(4...10)).join("\n") }
|
50
|
-
end
|
51
|
-
|
52
48
|
factory :person do
|
53
49
|
name { "#{Faker::Name.first_name} #{Faker::Name.last_name}" }
|
54
50
|
end
|
@@ -1,9 +1,10 @@
|
|
1
1
|
module Avo
|
2
2
|
module Fields
|
3
3
|
class BelongsToField < BaseField
|
4
|
+
attr_reader :searchable
|
4
5
|
attr_reader :polymorphic_as
|
5
6
|
attr_reader :relation_method
|
6
|
-
attr_reader :types
|
7
|
+
attr_reader :types
|
7
8
|
|
8
9
|
def initialize(id, **args, &block)
|
9
10
|
args[:placeholder] ||= I18n.t("avo.choose_an_option")
|
@@ -16,28 +17,10 @@ module Avo
|
|
16
17
|
@relation_method = name.to_s.parameterize.underscore
|
17
18
|
end
|
18
19
|
|
19
|
-
def searchable
|
20
|
-
@searchable && ::Avo::App.license.has_with_trial(:searchable_belongs_to)
|
21
|
-
end
|
22
|
-
|
23
20
|
def value
|
24
21
|
super(polymorphic_as)
|
25
22
|
end
|
26
23
|
|
27
|
-
# The value
|
28
|
-
def field_value
|
29
|
-
value.send(database_value)
|
30
|
-
rescue
|
31
|
-
nil
|
32
|
-
end
|
33
|
-
|
34
|
-
# What the user sees in the text field
|
35
|
-
def field_label
|
36
|
-
value.send(target_resource.class.title)
|
37
|
-
rescue
|
38
|
-
nil
|
39
|
-
end
|
40
|
-
|
41
24
|
def options
|
42
25
|
::Avo::Services::AuthorizationService.apply_policy(user, target_resource.class.query_scope).all.map do |model|
|
43
26
|
{
|
@@ -1,15 +1,14 @@
|
|
1
1
|
module Avo
|
2
2
|
module Fields
|
3
3
|
class FilesField < BaseField
|
4
|
-
attr_accessor :is_audio
|
5
4
|
attr_accessor :is_image
|
6
5
|
attr_accessor :direct_upload
|
7
6
|
|
8
7
|
def initialize(id, **args, &block)
|
9
8
|
super(id, **args, &block)
|
10
9
|
|
11
|
-
@is_audio = args[:is_audio].present? ? args[:is_audio] : false
|
12
10
|
@is_image = args[:is_image].present? ? args[:is_image] : @is_avatar
|
11
|
+
@is_audio = args[:is_audio].present? ? args[:is_audio] : false
|
13
12
|
@direct_upload = args[:direct_upload].present? ? args[:direct_upload] : false
|
14
13
|
end
|
15
14
|
|
data/lib/avo/version.rb
CHANGED