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.

Files changed (36) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +5 -3
  4. data/app/assets/svgs/x.svg +3 -0
  5. data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +22 -0
  6. data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +33 -0
  7. data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +39 -33
  8. data/app/components/avo/fields/common/files_list_viewer_component.html.erb +1 -1
  9. data/app/components/avo/fields/common/multiple_file_viewer_component.html.erb +2 -0
  10. data/app/components/avo/fields/common/multiple_file_viewer_component.rb +2 -1
  11. data/app/components/avo/fields/common/single_file_viewer_component.html.erb +2 -0
  12. data/app/components/avo/fields/common/single_file_viewer_component.rb +2 -1
  13. data/app/components/avo/fields/file_field/edit_component.html.erb +1 -1
  14. data/app/components/avo/fields/file_field/index_component.html.erb +3 -1
  15. data/app/components/avo/fields/file_field/show_component.html.erb +1 -1
  16. data/app/components/avo/resource_component.rb +1 -0
  17. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  18. data/app/controllers/avo/relations_controller.rb +4 -4
  19. data/app/controllers/avo/search_controller.rb +0 -1
  20. data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +96 -33
  21. data/app/javascript/js/controllers/search_controller.js +83 -17
  22. data/app/views/avo/partials/_global_search.html.erb +0 -1
  23. data/app/views/avo/partials/_javascript.html.erb +1 -0
  24. data/app/views/avo/partials/_resource_search.html.erb +0 -1
  25. data/db/factories.rb +4 -0
  26. data/lib/avo/fields/base_field.rb +1 -1
  27. data/lib/avo/fields/belongs_to_field.rb +19 -2
  28. data/lib/avo/fields/file_field.rb +2 -0
  29. data/lib/avo/fields/files_field.rb +2 -0
  30. data/lib/avo/licensing/pro_license.rb +2 -1
  31. data/lib/avo/version.rb +1 -1
  32. data/lib/generators/avo/templates/locales/avo.en.yml +1 -0
  33. data/public/avo-assets/avo.css +16 -0
  34. data/public/avo-assets/avo.js +317 -234
  35. data/public/avo-assets/avo.js.map +2 -2
  36. 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 = ['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
@@ -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
@@ -7,7 +7,8 @@ module Avo
7
7
  :custom_tools,
8
8
  :custom_fields,
9
9
  :global_search,
10
- :enhanced_search_results
10
+ :enhanced_search_results,
11
+ :searchable_belongs_to,
11
12
  ]
12
13
  end
13
14
  end
data/lib/avo/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "1.20.1"
2
+ VERSION = "1.21.0"
3
3
  end
@@ -93,3 +93,4 @@ en:
93
93
  delete_row: 'Delete row'
94
94
  was_successfully_created: 'was successfully created'
95
95
  was_successfully_updated: 'was successfully updated'
96
+ clear_value: "Clear value"
@@ -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
  }