avo 1.21.0.pre.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 -5
- 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/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/files_field.rb +2 -1
- 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 +7 -7
- data/app/assets/builds/avo.css +0 -8590
- data/app/assets/builds/avo.js +0 -87755
- data/app/assets/builds/avo.js.map +0 -7
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: ad998ae9691ee49081421f3db40c867fadf49c16d692722723277b43a631a1f7
|
4
|
+
data.tar.gz: 572ca7ed862e4e8bc3a784ed6c47402b5725ca9e99e286104f70739263e89590
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 3d69b2cb4c5691376fb8b3e7fcb26fabadde724d1de17fd015d03b94ddb4f833423bd018f033db10b02ad0c773d9151199d9c63acccdc59b707c9f8b18d42d8c
|
7
|
+
data.tar.gz: 40d1d605818269b2af621b3c4640a35117e4f53463151c0e339860ca763d8d8a04a7df4a56c1fa4f43ad8e8fa039ef61518055015bcc4d194cfaddcf430f048e
|
data/Gemfile
CHANGED
@@ -30,7 +30,7 @@ gem "rails", "~> 6.1.0"
|
|
30
30
|
# Use postgresql as the database for Active Record
|
31
31
|
gem "pg", ">= 0.18", "< 2.0"
|
32
32
|
# Use Puma as the app server
|
33
|
-
gem "puma", "~> 5.
|
33
|
+
gem "puma", "~> 5.6.2"
|
34
34
|
# Build JSON APIs with ease. Read more: https://github.com/rails/jbuilder
|
35
35
|
# gem "jbuilder", "~> 2.7"
|
36
36
|
# Use Redis adapter to run Action Cable in production
|
@@ -77,6 +77,8 @@ group :development do
|
|
77
77
|
# gem 'ruby-prof'
|
78
78
|
|
79
79
|
# gem 'pry-rails'
|
80
|
+
|
81
|
+
gem 'htmlbeautifier'
|
80
82
|
end
|
81
83
|
|
82
84
|
group :development, :test do
|
data/Gemfile.lock
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
avo (1.21.0
|
4
|
+
avo (1.21.0)
|
5
5
|
active_link_to
|
6
6
|
addressable
|
7
7
|
breadcrumbs_on_rails
|
@@ -175,6 +175,7 @@ GEM
|
|
175
175
|
rails (>= 6.0.0)
|
176
176
|
stimulus-rails
|
177
177
|
turbo-rails
|
178
|
+
htmlbeautifier (1.4.1)
|
178
179
|
httparty (0.20.0)
|
179
180
|
mime-types (~> 3.0)
|
180
181
|
multi_xml (>= 0.5.2)
|
@@ -229,8 +230,6 @@ GEM
|
|
229
230
|
nokogiri (1.13.1)
|
230
231
|
mini_portile2 (~> 2.7.0)
|
231
232
|
racc (~> 1.4)
|
232
|
-
nokogiri (1.13.1-x86_64-linux)
|
233
|
-
racc (~> 1.4)
|
234
233
|
orm_adapter (0.5.0)
|
235
234
|
pagy (5.10.1)
|
236
235
|
activesupport
|
@@ -239,7 +238,7 @@ GEM
|
|
239
238
|
ast (~> 2.4.1)
|
240
239
|
pg (1.3.1)
|
241
240
|
public_suffix (4.0.6)
|
242
|
-
puma (5.
|
241
|
+
puma (5.6.2)
|
243
242
|
nio4r (~> 2.0)
|
244
243
|
pundit (2.1.1)
|
245
244
|
activesupport (>= 3.0.0)
|
@@ -419,6 +418,7 @@ DEPENDENCIES
|
|
419
418
|
fuubar
|
420
419
|
gem-release
|
421
420
|
hotwire-rails
|
421
|
+
htmlbeautifier
|
422
422
|
httparty
|
423
423
|
image_processing (~> 1.2)
|
424
424
|
iso
|
@@ -429,7 +429,7 @@ DEPENDENCIES
|
|
429
429
|
meta-tags
|
430
430
|
net-smtp
|
431
431
|
pg (>= 0.18, < 2.0)
|
432
|
-
puma (~> 5.
|
432
|
+
puma (~> 5.6.2)
|
433
433
|
pundit
|
434
434
|
rails (~> 6.1.0)
|
435
435
|
rails-controller-testing
|
@@ -0,0 +1,22 @@
|
|
1
|
+
<div data-controller="search" class="resource-search flex items-center h-full w-full" data-turbo-remove-before-cache>
|
2
|
+
<div class="w-full hidden"
|
3
|
+
data-search-target="autocomplete"
|
4
|
+
data-search-resource="<%= @model_key %>"
|
5
|
+
data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>"}'
|
6
|
+
data-via-association="belongs_to"
|
7
|
+
></div>
|
8
|
+
<div class="relative w-full" autocomplete="off">
|
9
|
+
<%= @form.text_field @field.foreign_key,
|
10
|
+
value: field_label,
|
11
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)), 'data-search-target': 'button clearValue',
|
12
|
+
placeholder: @field.placeholder,
|
13
|
+
disabled: true %>
|
14
|
+
<div class="absolute top-1/2 left-auto right-3 mr-px -mt-2 cursor-pointer hidden text-gray-500"
|
15
|
+
data-tippy="tooltip"
|
16
|
+
data-search-target="clearButton"
|
17
|
+
title="<%= I18n.translate 'avo.clear_value' %>"
|
18
|
+
data-action="click->search#clearValue"
|
19
|
+
><%= helpers.svg 'x', class: 'h-4' %></div>
|
20
|
+
</div>
|
21
|
+
<%= @form.hidden_field @foreign_key, value: field_value, 'data-search-target': 'hiddenId clearValue' %>
|
22
|
+
</div>
|
@@ -0,0 +1,33 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
class Avo::Fields::BelongsToField::AutocompleteComponent < ViewComponent::Base
|
4
|
+
def initialize(form:, field:, type: nil, model_key:, foreign_key:)
|
5
|
+
@form = form
|
6
|
+
@field = field
|
7
|
+
@type = type
|
8
|
+
@model_key = model_key
|
9
|
+
@foreign_key = foreign_key
|
10
|
+
end
|
11
|
+
|
12
|
+
def field_label
|
13
|
+
if searchable?
|
14
|
+
@field.value&.class == @type ? @field.field_label : nil
|
15
|
+
else
|
16
|
+
@field.field_label
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
def field_value
|
21
|
+
if searchable?
|
22
|
+
@field.value&.class == @type ? @field.field_value : nil
|
23
|
+
else
|
24
|
+
@field.field_value
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
def searchable?
|
31
|
+
@type.present?
|
32
|
+
end
|
33
|
+
end
|
@@ -1,22 +1,34 @@
|
|
1
|
-
<%
|
2
|
-
|
1
|
+
<%
|
2
|
+
if @field.types.present? # It's a polymorphic association
|
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
|
+
>
|
3
15
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
4
16
|
<%= @form.select "#{@field.foreign_key}_type", @field.types.map { |type| [type.to_s.underscore.humanize, type.to_s] },
|
5
17
|
{
|
18
|
+
value: @field.value,
|
6
19
|
include_blank: @field.placeholder,
|
7
20
|
},
|
8
21
|
{
|
9
22
|
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
10
23
|
disabled: disabled,
|
11
24
|
'data-belongs-to-field-target': "select",
|
12
|
-
'data-action': 'change->belongs-to-field#
|
25
|
+
'data-action': 'change->belongs-to-field#changeType'
|
13
26
|
}
|
14
27
|
%>
|
15
28
|
<%
|
16
29
|
# If the select field is disabled, no value will be sent. It's how HTML works.
|
17
30
|
# Thus the extra hidden field to actually send the related id to the server.
|
18
|
-
if disabled
|
19
|
-
%>
|
31
|
+
if disabled %>
|
20
32
|
<%= @form.hidden_field "#{@field.foreign_key}_type" %>
|
21
33
|
<% end %>
|
22
34
|
<% end %>
|
@@ -26,21 +38,18 @@
|
|
26
38
|
data-type="<%= type %>"
|
27
39
|
>
|
28
40
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal, label: type.to_s.underscore.humanize do %>
|
29
|
-
|
30
|
-
{
|
31
|
-
|
32
|
-
},
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
41
|
+
<% if @field.searchable %>
|
42
|
+
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form, field: @field, type: type, model_key: model_keys[type.to_s], foreign_key: "#{@field.foreign_key}_id" %>
|
43
|
+
<% else %>
|
44
|
+
<%= @form.select "#{@field.foreign_key}_id", options_for_select(@field.values_for_type(type), @field.value&.class == type ? @field.field_value : nil),
|
45
|
+
{
|
46
|
+
include_blank: @field.placeholder,
|
47
|
+
},
|
48
|
+
{
|
49
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
50
|
+
disabled: disabled
|
51
|
+
}
|
37
52
|
%>
|
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" %>
|
44
53
|
<% end %>
|
45
54
|
<% end %>
|
46
55
|
</div>
|
@@ -48,21 +57,18 @@
|
|
48
57
|
</div>
|
49
58
|
<% else %>
|
50
59
|
<%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
},
|
55
|
-
|
56
|
-
|
57
|
-
|
58
|
-
|
60
|
+
<% if @field.searchable %>
|
61
|
+
<%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: @form, field: @field, model_key: @field.target_resource&.model_key, foreign_key: @field.foreign_key %>
|
62
|
+
<% else %>
|
63
|
+
<%= @form.select @field.foreign_key, @field.options.map { |o| [o[:label], o[:value]] },
|
64
|
+
{
|
65
|
+
include_blank: @field.placeholder,
|
66
|
+
},
|
67
|
+
{
|
68
|
+
class: helpers.input_classes('w-full', has_error: @field.model_errors.include?(@field.id)),
|
69
|
+
disabled: disabled
|
70
|
+
}
|
59
71
|
%>
|
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 %>
|
66
72
|
<% end %>
|
67
73
|
<% end %>
|
68
74
|
<% 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_key} 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? %>
|
@@ -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, only: [:show, :index, :new, :create, :destroy]
|
8
|
+
before_action :hydrate_related_resource, only: [:show, :index, :new, :create, :destroy]
|
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, only: [:show, :index, :new, :create, :destroy]
|
11
|
+
before_action :set_attachment_resource, only: [:show, :index, :new, :create, :destroy]
|
12
12
|
before_action :set_attachment_model, only: [:create, :destroy]
|
13
13
|
before_action :set_reflection, only: [:index, :show]
|
14
14
|
|
@@ -1,65 +1,128 @@
|
|
1
1
|
import { Controller } from 'stimulus'
|
2
2
|
|
3
3
|
export default class extends Controller {
|
4
|
-
static targets = ['select', 'type']
|
4
|
+
static targets = ['select', 'type', 'loadAssociationLink'];
|
5
|
+
|
6
|
+
defaults = {};
|
5
7
|
|
6
8
|
get selectedType() {
|
7
9
|
return this.selectTarget.value
|
8
10
|
}
|
9
11
|
|
10
|
-
|
11
|
-
this.
|
12
|
-
this.changedType()
|
12
|
+
get isSearchable() {
|
13
|
+
return this.context.scope.element.dataset.searchable === 'true'
|
13
14
|
}
|
14
15
|
|
15
|
-
|
16
|
-
this.
|
17
|
-
|
18
|
-
const select = target.querySelector('select')
|
19
|
-
const name = select.getAttribute('name')
|
16
|
+
get association() {
|
17
|
+
return this.context.scope.element.dataset.association
|
18
|
+
}
|
20
19
|
|
21
|
-
|
22
|
-
|
23
|
-
select.selectedIndex = 0
|
24
|
-
}
|
25
|
-
})
|
20
|
+
get associationClass() {
|
21
|
+
return this.context.scope.element.dataset.associationClass
|
26
22
|
}
|
27
23
|
|
28
|
-
|
29
|
-
this.
|
30
|
-
this.
|
24
|
+
connect() {
|
25
|
+
this.copyValidNames()
|
26
|
+
this.changeType() // Do the initial type change
|
31
27
|
}
|
32
28
|
|
33
|
-
|
29
|
+
changeType() {
|
30
|
+
this.hideAllTypes()
|
31
|
+
this.showType(this.selectTarget.value)
|
32
|
+
}
|
33
|
+
|
34
|
+
// Private
|
35
|
+
|
36
|
+
hideAllTypes() {
|
34
37
|
this.typeTargets.forEach((target) => {
|
35
|
-
|
38
|
+
target.classList.add('hidden')
|
39
|
+
|
36
40
|
this.invalidateTarget(target)
|
37
41
|
})
|
38
42
|
}
|
39
43
|
|
40
|
-
|
41
|
-
|
42
|
-
|
44
|
+
/**
|
45
|
+
* Used for invalidating select fields when switching between types so they don't automatically override the previous id.
|
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
|
+
*/
|
43
51
|
|
44
|
-
|
45
|
-
|
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
|
46
58
|
|
47
|
-
|
48
|
-
|
59
|
+
if (this.isSearchable) {
|
60
|
+
const textInput = target.querySelector('input[type="text"]')
|
61
|
+
if (textInput) {
|
62
|
+
textInput.setAttribute('valid-name', textInput.getAttribute('name'))
|
63
|
+
}
|
49
64
|
|
50
|
-
|
51
|
-
|
52
|
-
|
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
|
+
}
|
53
77
|
|
54
|
-
|
78
|
+
if (this.selectedType !== type) {
|
79
|
+
select.selectedIndex = 0
|
80
|
+
}
|
81
|
+
}
|
82
|
+
})
|
55
83
|
}
|
56
84
|
|
57
|
-
|
58
|
-
const target = this.typeTargets.find(
|
59
|
-
|
85
|
+
showType(type) {
|
86
|
+
const target = this.typeTargets.find(
|
87
|
+
(typeTarget) => typeTarget.dataset.type === type,
|
88
|
+
)
|
60
89
|
if (target) {
|
61
90
|
target.classList.remove('hidden')
|
91
|
+
|
62
92
|
this.validateTarget(target)
|
63
93
|
}
|
64
94
|
}
|
95
|
+
|
96
|
+
/**
|
97
|
+
* Copy value from `valid-name` to `name`
|
98
|
+
*/
|
99
|
+
validateTarget(target) {
|
100
|
+
if (this.isSearchable) {
|
101
|
+
const textInput = target.querySelector('input[type="text"]')
|
102
|
+
const hiddenInput = target.querySelector('input[type="hidden"]')
|
103
|
+
|
104
|
+
textInput.setAttribute('name', textInput.getAttribute('valid-name'))
|
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
|
+
}
|
110
|
+
}
|
111
|
+
|
112
|
+
/**
|
113
|
+
* nullify the `name` attribute
|
114
|
+
*/
|
115
|
+
invalidateTarget(target) {
|
116
|
+
if (this.isSearchable) {
|
117
|
+
// Wrapping it in a try/catch to counter turbo's cache system (going back to the edit page after initial save)
|
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) {}
|
126
|
+
}
|
127
|
+
}
|
65
128
|
}
|
@@ -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
|