avo 1.20.1 → 1.21.0
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 +3 -1
- data/Gemfile.lock +5 -3
- data/app/assets/svgs/x.svg +3 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +22 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +33 -0
- data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +39 -33
- data/app/components/avo/fields/common/files_list_viewer_component.html.erb +1 -1
- data/app/components/avo/fields/common/multiple_file_viewer_component.html.erb +2 -0
- data/app/components/avo/fields/common/multiple_file_viewer_component.rb +2 -1
- data/app/components/avo/fields/common/single_file_viewer_component.html.erb +2 -0
- data/app/components/avo/fields/common/single_file_viewer_component.rb +2 -1
- data/app/components/avo/fields/file_field/edit_component.html.erb +1 -1
- data/app/components/avo/fields/file_field/index_component.html.erb +3 -1
- data/app/components/avo/fields/file_field/show_component.html.erb +1 -1
- data/app/components/avo/resource_component.rb +1 -0
- data/app/components/avo/views/resource_index_component.html.erb +1 -1
- data/app/controllers/avo/relations_controller.rb +4 -4
- data/app/controllers/avo/search_controller.rb +0 -1
- data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +96 -33
- data/app/javascript/js/controllers/search_controller.js +83 -17
- data/app/views/avo/partials/_global_search.html.erb +0 -1
- data/app/views/avo/partials/_javascript.html.erb +1 -0
- data/app/views/avo/partials/_resource_search.html.erb +0 -1
- data/db/factories.rb +4 -0
- data/lib/avo/fields/base_field.rb +1 -1
- data/lib/avo/fields/belongs_to_field.rb +19 -2
- data/lib/avo/fields/file_field.rb +2 -0
- data/lib/avo/fields/files_field.rb +2 -0
- data/lib/avo/licensing/pro_license.rb +2 -1
- data/lib/avo/version.rb +1 -1
- data/lib/generators/avo/templates/locales/avo.en.yml +1 -0
- data/public/avo-assets/avo.css +16 -0
- data/public/avo-assets/avo.js +317 -234
- data/public/avo-assets/avo.js.map +2 -2
- metadata +5 -2
@@ -3,17 +3,40 @@ 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'
|
6
7
|
import debouncePromise from '../helpers/debounce_promise'
|
7
8
|
|
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
|
+
*/
|
8
16
|
export default class extends Controller {
|
9
|
-
static targets = [
|
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
|
+
}
|
10
31
|
|
11
|
-
|
32
|
+
get searchDebounce() {
|
33
|
+
return window.Avo.configuration.search_debounce
|
34
|
+
}
|
12
35
|
|
13
36
|
get translationKeys() {
|
14
37
|
let keys
|
15
38
|
try {
|
16
|
-
keys = JSON.parse(this.
|
39
|
+
keys = JSON.parse(this.dataset.translationKeys)
|
17
40
|
} catch (error) {
|
18
41
|
keys = {}
|
19
42
|
}
|
@@ -21,20 +44,48 @@ export default class extends Controller {
|
|
21
44
|
return keys
|
22
45
|
}
|
23
46
|
|
24
|
-
get
|
25
|
-
return this.
|
47
|
+
get isBelongsToSearch() {
|
48
|
+
return this.dataset.viaAssociation === 'belongs_to'
|
26
49
|
}
|
27
50
|
|
28
|
-
get
|
29
|
-
return this.
|
51
|
+
get isGlobalSearch() {
|
52
|
+
return this.dataset.searchResource === 'global'
|
30
53
|
}
|
31
54
|
|
32
|
-
|
33
|
-
|
55
|
+
searchUrl(query) {
|
56
|
+
const url = URI()
|
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()
|
34
76
|
}
|
35
77
|
|
36
|
-
|
37
|
-
|
78
|
+
handleOnSelect({ item }) {
|
79
|
+
if (this.isBelongsToSearch) {
|
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
|
+
}
|
38
89
|
}
|
39
90
|
|
40
91
|
addSource(resourceName, data) {
|
@@ -43,9 +94,7 @@ export default class extends Controller {
|
|
43
94
|
return {
|
44
95
|
sourceId: resourceName,
|
45
96
|
getItems: () => data.results,
|
46
|
-
onSelect(
|
47
|
-
Turbo.visit(item._url, { action: 'replace' })
|
48
|
-
},
|
97
|
+
onSelect: that.handleOnSelect.bind(that),
|
49
98
|
templates: {
|
50
99
|
header() {
|
51
100
|
return `${data.header.toUpperCase()} ${data.help}`
|
@@ -84,7 +133,10 @@ export default class extends Controller {
|
|
84
133
|
})
|
85
134
|
},
|
86
135
|
noResults() {
|
87
|
-
return that.translationKeys.no_item_found.replace(
|
136
|
+
return that.translationKeys.no_item_found.replace(
|
137
|
+
'%{item}',
|
138
|
+
resourceName,
|
139
|
+
)
|
88
140
|
},
|
89
141
|
},
|
90
142
|
}
|
@@ -94,11 +146,22 @@ export default class extends Controller {
|
|
94
146
|
this.autocompleteTarget.querySelector('button').click()
|
95
147
|
}
|
96
148
|
|
149
|
+
clearValue() {
|
150
|
+
this.clearValueTargets.map((e) => e.setAttribute('value', ''))
|
151
|
+
this.clearButtonTarget.classList.add('hidden')
|
152
|
+
}
|
153
|
+
|
97
154
|
connect() {
|
98
155
|
const that = this
|
99
156
|
|
100
157
|
this.buttonTarget.onclick = () => this.showSearchPanel()
|
101
158
|
|
159
|
+
this.clearValueTargets.forEach((target) => {
|
160
|
+
if (target.getAttribute('value')) {
|
161
|
+
this.clearButtonTarget.classList.remove('hidden')
|
162
|
+
}
|
163
|
+
})
|
164
|
+
|
102
165
|
if (this.isGlobalSearch) {
|
103
166
|
Mousetrap.bind(['command+k', 'ctrl+k'], () => this.showSearchPanel())
|
104
167
|
}
|
@@ -112,12 +175,15 @@ export default class extends Controller {
|
|
112
175
|
openOnFocus: true,
|
113
176
|
detachedMediaQuery: '',
|
114
177
|
getSources: ({ query }) => {
|
115
|
-
const endpoint =
|
178
|
+
const endpoint = that.searchUrl(query)
|
116
179
|
|
117
|
-
return that
|
180
|
+
return that
|
181
|
+
.debouncedFetch(endpoint)
|
118
182
|
.then((response) => response.json())
|
119
183
|
.then((data) => Object.keys(data).map((resourceName) => that.addSource(resourceName, data[resourceName])))
|
120
184
|
},
|
121
185
|
})
|
186
|
+
|
187
|
+
this.buttonTarget.removeAttribute('disabled')
|
122
188
|
}
|
123
189
|
}
|
@@ -3,7 +3,6 @@
|
|
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 %>'
|
7
6
|
>
|
8
7
|
</div>
|
9
8
|
<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,7 +3,6 @@
|
|
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 %>'
|
7
6
|
>
|
8
7
|
</div>
|
9
8
|
<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,6 +45,10 @@ 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
|
+
|
48
52
|
factory :person do
|
49
53
|
name { "#{Faker::Name.first_name} #{Faker::Name.last_name}" }
|
50
54
|
end
|
@@ -1,10 +1,9 @@
|
|
1
1
|
module Avo
|
2
2
|
module Fields
|
3
3
|
class BelongsToField < BaseField
|
4
|
-
attr_reader :searchable
|
5
4
|
attr_reader :polymorphic_as
|
6
5
|
attr_reader :relation_method
|
7
|
-
attr_reader :types
|
6
|
+
attr_reader :types # for Polymorphic associations
|
8
7
|
|
9
8
|
def initialize(id, **args, &block)
|
10
9
|
args[:placeholder] ||= I18n.t("avo.choose_an_option")
|
@@ -17,10 +16,28 @@ module Avo
|
|
17
16
|
@relation_method = name.to_s.parameterize.underscore
|
18
17
|
end
|
19
18
|
|
19
|
+
def searchable
|
20
|
+
@searchable && ::Avo::App.license.has_with_trial(:searchable_belongs_to)
|
21
|
+
end
|
22
|
+
|
20
23
|
def value
|
21
24
|
super(polymorphic_as)
|
22
25
|
end
|
23
26
|
|
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
|
+
|
24
41
|
def options
|
25
42
|
::Avo::Services::AuthorizationService.apply_policy(user, target_resource.class.query_scope).all.map do |model|
|
26
43
|
{
|
@@ -4,6 +4,7 @@ module Avo
|
|
4
4
|
attr_accessor :link_to_resource
|
5
5
|
attr_accessor :is_avatar
|
6
6
|
attr_accessor :is_image
|
7
|
+
attr_accessor :is_audio
|
7
8
|
attr_accessor :direct_upload
|
8
9
|
|
9
10
|
def initialize(id, **args, &block)
|
@@ -12,6 +13,7 @@ module Avo
|
|
12
13
|
@link_to_resource = args[:link_to_resource].present? ? args[:link_to_resource] : false
|
13
14
|
@is_avatar = args[:is_avatar].present? ? args[:is_avatar] : false
|
14
15
|
@is_image = args[:is_image].present? ? args[:is_image] : @is_avatar
|
16
|
+
@is_audio = args[:is_audio].present? ? args[:is_audio] : false
|
15
17
|
@direct_upload = args[:direct_upload].present? ? args[:direct_upload] : false
|
16
18
|
end
|
17
19
|
|
@@ -1,12 +1,14 @@
|
|
1
1
|
module Avo
|
2
2
|
module Fields
|
3
3
|
class FilesField < BaseField
|
4
|
+
attr_accessor :is_audio
|
4
5
|
attr_accessor :is_image
|
5
6
|
attr_accessor :direct_upload
|
6
7
|
|
7
8
|
def initialize(id, **args, &block)
|
8
9
|
super(id, **args, &block)
|
9
10
|
|
11
|
+
@is_audio = args[:is_audio].present? ? args[:is_audio] : false
|
10
12
|
@is_image = args[:is_image].present? ? args[:is_image] : @is_avatar
|
11
13
|
@direct_upload = args[:direct_upload].present? ? args[:direct_upload] : false
|
12
14
|
end
|
data/lib/avo/version.rb
CHANGED
data/public/avo-assets/avo.css
CHANGED
@@ -6402,6 +6402,14 @@ progress[value]::-moz-progress-bar{
|
|
6402
6402
|
top:-1px
|
6403
6403
|
}
|
6404
6404
|
|
6405
|
+
.left-auto{
|
6406
|
+
left:auto
|
6407
|
+
}
|
6408
|
+
|
6409
|
+
.right-3{
|
6410
|
+
right:0.75rem
|
6411
|
+
}
|
6412
|
+
|
6405
6413
|
.z-10{
|
6406
6414
|
z-index:10
|
6407
6415
|
}
|
@@ -6571,6 +6579,14 @@ progress[value]::-moz-progress-bar{
|
|
6571
6579
|
margin-left:0.5rem
|
6572
6580
|
}
|
6573
6581
|
|
6582
|
+
.mr-px{
|
6583
|
+
margin-right:1px
|
6584
|
+
}
|
6585
|
+
|
6586
|
+
.-mt-2{
|
6587
|
+
margin-top:-0.5rem
|
6588
|
+
}
|
6589
|
+
|
6574
6590
|
.ml-6{
|
6575
6591
|
margin-left:1.5rem
|
6576
6592
|
}
|