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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 2009c14b639e720209ef1c2d56c84ac7936cfa96cb71dd4306b02695cae3ade5
4
- data.tar.gz: 4e99a479bc58b1da03e2e13f63e1921b1e652724b2d5b8d394964dad6a43a8ae
3
+ metadata.gz: ad998ae9691ee49081421f3db40c867fadf49c16d692722723277b43a631a1f7
4
+ data.tar.gz: 572ca7ed862e4e8bc3a784ed6c47402b5725ca9e99e286104f70739263e89590
5
5
  SHA512:
6
- metadata.gz: 91a4af05f9cc03bdf76751a140697a0ef6d5782f5a049e39023d7dffba3074c16d01204f36608c2f8d776b6f3f72d885bd91f49b6e38ab14ac7b951df13667c9
7
- data.tar.gz: 18d41c5e64b507ab27025b4af7fe7b99987d45897b6e717022fcebe8e5debd32dab26aa96a24611a8534ffcaa0f6744932d277d37421e1865722b0f728234516
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.5.1"
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.pre.1)
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.5.2)
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.5.1)
432
+ puma (~> 5.6.2)
433
433
  pundit
434
434
  rails (~> 6.1.0)
435
435
  rails-controller-testing
@@ -0,0 +1,3 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" class="h-6 w-6" fill="none" viewBox="0 0 24 24" stroke="currentColor">
2
+ <path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
3
+ </svg>
@@ -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
- <% if @field.types.present? %>
2
- <div data-controller="belongs-to-field">
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#changedType'
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
- <%= @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
- }
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
- <%= @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
- }
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.model_class.model_name.plural} if @resource.search_query.present? %>
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
 
@@ -60,7 +60,6 @@ module Avo
60
60
  _id: model.id,
61
61
  _label: resource.label,
62
62
  _url: resource.record_path,
63
- model: model
64
63
  }
65
64
 
66
65
  if App.license.has_with_trial(:enhanced_search_results)
@@ -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
- connect() {
11
- this.setValidNames()
12
- this.changedType()
12
+ get isSearchable() {
13
+ return this.context.scope.element.dataset.searchable === 'true'
13
14
  }
14
15
 
15
- setValidNames() {
16
- this.typeTargets.forEach((target) => {
17
- const { type } = target.dataset
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
- select.setAttribute('valid-name', name)
22
- if (this.selectedType !== type) {
23
- select.selectedIndex = 0
24
- }
25
- })
20
+ get associationClass() {
21
+ return this.context.scope.element.dataset.associationClass
26
22
  }
27
23
 
28
- changedType() {
29
- this.hideAllTypeTargets()
30
- this.enableType(this.selectTarget.value)
24
+ connect() {
25
+ this.copyValidNames()
26
+ this.changeType() // Do the initial type change
31
27
  }
32
28
 
33
- hideAllTypeTargets() {
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
- this.hideTarget(target)
38
+ target.classList.add('hidden')
39
+
36
40
  this.invalidateTarget(target)
37
41
  })
38
42
  }
39
43
 
40
- hideTarget(target) {
41
- target.classList.add('hidden')
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
- invalidateTarget(target) {
45
- const select = target.querySelector('select')
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
- select.setAttribute('name', '')
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
- validateTarget(target) {
51
- const select = target.querySelector('select')
52
- const validName = select.getAttribute('valid-name')
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
- select.setAttribute('name', validName)
78
+ if (this.selectedType !== type) {
79
+ select.selectedIndex = 0
80
+ }
81
+ }
82
+ })
55
83
  }
56
84
 
57
- enableType(type) {
58
- const target = this.typeTargets.find((typeTarget) => typeTarget.dataset.type === type)
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 = ['autocomplete', 'button']
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
- debouncedFetch = debouncePromise(fetch, this.debounceTimeout)
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.autocompleteTarget.dataset.translationKeys)
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 debounceTimeout() {
25
- return this.autocompleteTarget.dataset.debounceTimeout
47
+ get isBelongsToSearch() {
48
+ return this.dataset.viaAssociation === 'belongs_to'
26
49
  }
27
50
 
28
- get searchResource() {
29
- return this.autocompleteTarget.dataset.searchResource
51
+ get isGlobalSearch() {
52
+ return this.dataset.searchResource === 'global'
30
53
  }
31
54
 
32
- get isGlobalSearch() {
33
- return this.searchResource === 'global'
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
- get searchUrl() {
37
- return this.isGlobalSearch ? `${window.Avo.configuration.root_path}/avo_api/search` : `${window.Avo.configuration.root_path}/avo_api/${this.searchResource}/search`
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({ item }) {
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('%{item}', resourceName)
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 = `${that.searchUrl}?q=${query}`
178
+ const endpoint = that.searchUrl(query)
116
179
 
117
- return that.debouncedFetch(endpoint)
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"
@@ -2,4 +2,5 @@
2
2
  window.Avo = window.Avo || { configuration: {} }
3
3
  Avo.configuration.timezone = '<%= Avo.configuration.timezone %>'
4
4
  Avo.configuration.root_path = '<%= Avo::App.root_path %>'
5
+ Avo.configuration.search_debounce = '<%= Avo.configuration.search_debounce %>'
5
6
  <% end %>
@@ -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
@@ -136,7 +136,7 @@ module Avo
136
136
  end
137
137
  end
138
138
 
139
- # Run callback block if present
139
+ # Run computable callback block if present
140
140
  if computable && block.present?
141
141
  final_value = instance_exec(@model, @resource, @view, self, &block)
142
142
  end