avo 1.21.0.pre.1 → 1.22.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -1
  3. data/Gemfile.lock +61 -61
  4. data/README.md +1 -1
  5. data/app/assets/svgs/x.svg +3 -0
  6. data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +29 -0
  7. data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +77 -0
  8. data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +73 -46
  9. data/app/components/avo/fields/belongs_to_field/edit_component.rb +37 -0
  10. data/app/components/avo/navigation_link_component.html.erb +1 -1
  11. data/app/components/avo/navigation_link_component.rb +2 -1
  12. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  13. data/app/controllers/avo/application_controller.rb +16 -14
  14. data/app/controllers/avo/base_controller.rb +1 -4
  15. data/app/controllers/avo/relations_controller.rb +4 -4
  16. data/app/controllers/avo/search_controller.rb +0 -1
  17. data/app/javascript/js/controllers/fields/belongs_to_field_controller.js +96 -33
  18. data/app/javascript/js/controllers/fields/date_field_controller.js +10 -2
  19. data/app/javascript/js/controllers/item_selector_controller.js +29 -19
  20. data/app/javascript/js/controllers/search_controller.js +88 -17
  21. data/app/views/avo/partials/_global_search.html.erb +0 -1
  22. data/app/views/avo/partials/_javascript.html.erb +1 -0
  23. data/app/views/avo/partials/_logo.html.erb +3 -1
  24. data/app/views/avo/partials/_resource_search.html.erb +0 -1
  25. data/app/views/avo/partials/_turbo_frame_wrap.html.erb +7 -1
  26. data/app/views/avo/sidebar/_sidebar.html.erb +1 -3
  27. data/db/factories.rb +11 -3
  28. data/lib/avo/base_resource.rb +16 -14
  29. data/lib/avo/fields/base_field.rb +1 -1
  30. data/lib/avo/fields/belongs_to_field.rb +19 -2
  31. data/lib/avo/fields/files_field.rb +2 -1
  32. data/lib/avo/fields/key_value_field.rb +28 -8
  33. data/lib/avo/licensing/pro_license.rb +2 -1
  34. data/lib/avo/version.rb +1 -1
  35. data/lib/generators/avo/templates/locales/avo.en.yml +2 -0
  36. data/lib/generators/avo/templates/locales/avo.nb-NO.yml +1 -0
  37. data/lib/generators/avo/templates/locales/avo.pt-BR.yml +1 -0
  38. data/lib/generators/avo/templates/locales/avo.ro.yml +1 -0
  39. data/public/avo-assets/avo.css +20 -4
  40. data/public/avo-assets/avo.js +330 -237
  41. data/public/avo-assets/avo.js.map +2 -2
  42. metadata +7 -7
  43. data/app/assets/builds/avo.css +0 -8590
  44. data/app/assets/builds/avo.js +0 -87755
  45. data/app/assets/builds/avo.js.map +0 -7
@@ -7,6 +7,7 @@ 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]
10
11
  before_action :authorize_action
11
12
  before_action :reset_pagination_if_filters_changed, only: :index
12
13
  before_action :cache_applied_filters, only: :index
@@ -112,7 +113,6 @@ module Avo
112
113
 
113
114
  def create
114
115
  # 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,7 +166,6 @@ module Avo
166
166
 
167
167
  def update
168
168
  # model gets instantiated and filled in the fill_model method
169
- fill_model
170
169
  saved = @model.save
171
170
  @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
172
171
 
@@ -194,8 +193,6 @@ module Avo
194
193
  private
195
194
 
196
195
  def model_params
197
- model_param_key = @resource.form_scope
198
-
199
196
  request_params = params.require(model_param_key).permit(permitted_params)
200
197
 
201
198
  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, 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
  }
@@ -1,8 +1,14 @@
1
1
  import { Controller } from 'stimulus'
2
2
  import { DateTime } from 'luxon'
3
- import { castBoolean } from '../../helpers/cast_boolean'
4
3
  import flatpickr from 'flatpickr'
5
4
 
5
+ import { castBoolean } from '../../helpers/cast_boolean'
6
+
7
+ // Get the DateTime with the TZ offset applied.
8
+ function universalTimestamp(timestampStr) {
9
+ return new Date(new Date(timestampStr).getTime() + (new Date(timestampStr).getTimezoneOffset() * 60 * 1000))
10
+ }
11
+
6
12
  export default class extends Controller {
7
13
  static targets = ['input']
8
14
 
@@ -43,7 +49,9 @@ export default class extends Controller {
43
49
  // this.timezone = Intl.DateTimeFormat().resolvedOptions().timeZone
44
50
  options.appTimezone = this.inputTarget.dataset.timezone
45
51
  } else {
46
- currentValue = new Date(this.inputTarget.value)
52
+ // Because the browser treats the date like a timestamp and updates it ot 00:00 hour, when on a western timezone the date will be converted with one day offset.
53
+ // Ex: 2022-01-30 will render as 2022-01-29 on an American timezone
54
+ currentValue = universalTimestamp(this.inputTarget.value)
47
55
  }
48
56
 
49
57
  options.defaultDate = currentValue
@@ -1,9 +1,9 @@
1
1
  import { Controller } from 'stimulus'
2
2
 
3
3
  export default class extends Controller {
4
- static targets = ['panel']
4
+ static targets = ['panel'];
5
5
 
6
- checkbox = {}
6
+ checkbox = {};
7
7
 
8
8
  get actionsPanelPresent() {
9
9
  return this.actionsButtonElement !== null
@@ -17,6 +17,12 @@ export default class extends Controller {
17
17
  }
18
18
  }
19
19
 
20
+ get actionLinks() {
21
+ return document.querySelectorAll(
22
+ '.js-actions-dropdown a[data-actions-picker-target="resourceAction"]',
23
+ )
24
+ }
25
+
20
26
  set currentIds(value) {
21
27
  this.stateHolderElement.dataset.selectedResources = JSON.stringify(value)
22
28
 
@@ -32,8 +38,12 @@ export default class extends Controller {
32
38
  connect() {
33
39
  this.resourceName = this.element.dataset.resourceName
34
40
  this.resourceId = this.element.dataset.resourceId
35
- this.actionsButtonElement = document.querySelector(`[data-actions-dropdown-button="${this.resourceName}"]`)
36
- this.stateHolderElement = document.querySelector(`[data-selected-resources-name="${this.resourceName}"]`)
41
+ this.actionsButtonElement = document.querySelector(
42
+ `[data-actions-dropdown-button="${this.resourceName}"]`,
43
+ )
44
+ this.stateHolderElement = document.querySelector(
45
+ `[data-selected-resources-name="${this.resourceName}"]`,
46
+ )
37
47
  }
38
48
 
39
49
  addToSelected() {
@@ -45,7 +55,9 @@ export default class extends Controller {
45
55
  }
46
56
 
47
57
  removeFromSelected() {
48
- this.currentIds = this.currentIds.filter((item) => item.toString() !== this.resourceId)
58
+ this.currentIds = this.currentIds.filter(
59
+ (item) => item.toString() !== this.resourceId,
60
+ )
49
61
  }
50
62
 
51
63
  toggle(event) {
@@ -59,22 +71,20 @@ export default class extends Controller {
59
71
  }
60
72
 
61
73
  enableResourceActions() {
62
- (document.querySelectorAll('.js-actions-dropdown a[data-actions-picker-target="resourceAction"]'))
63
- .forEach((link) => {
64
- link.classList.add('text-gray-700')
65
- link.classList.remove('text-gray-500')
66
- link.setAttribute('data-href', link.getAttribute('href'))
67
- link.dataset.disabled = false
68
- })
74
+ this.actionLinks.forEach((link) => {
75
+ link.classList.add('text-gray-700')
76
+ link.classList.remove('text-gray-500')
77
+ link.setAttribute('data-href', link.getAttribute('href'))
78
+ link.dataset.disabled = false
79
+ })
69
80
  }
70
81
 
71
82
  disableResourceActions() {
72
- (document.querySelectorAll('.js-actions-dropdown a[data-actions-picker-target="resourceAction"]'))
73
- .forEach((link) => {
74
- link.classList.remove('text-gray-700')
75
- link.classList.add('text-gray-500')
76
- link.setAttribute('href', link.getAttribute('data-href'))
77
- link.dataset.disabled = true
78
- })
83
+ this.actionLinks.forEach((link) => {
84
+ link.classList.remove('text-gray-700')
85
+ link.classList.add('text-gray-500')
86
+ link.setAttribute('href', link.getAttribute('data-href'))
87
+ link.dataset.disabled = true
88
+ })
79
89
  }
80
90
  }
@@ -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,50 @@ 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
+ if (this.hasClearButtonTarget) {
86
+ this.clearButtonTarget.classList.remove('hidden')
87
+ }
88
+ } else {
89
+ Turbo.visit(item._url, { action: 'advance' })
90
+ }
38
91
  }
39
92
 
40
93
  addSource(resourceName, data) {
@@ -43,9 +96,7 @@ export default class extends Controller {
43
96
  return {
44
97
  sourceId: resourceName,
45
98
  getItems: () => data.results,
46
- onSelect({ item }) {
47
- Turbo.visit(item._url, { action: 'replace' })
48
- },
99
+ onSelect: that.handleOnSelect.bind(that),
49
100
  templates: {
50
101
  header() {
51
102
  return `${data.header.toUpperCase()} ${data.help}`
@@ -84,7 +135,10 @@ export default class extends Controller {
84
135
  })
85
136
  },
86
137
  noResults() {
87
- return that.translationKeys.no_item_found.replace('%{item}', resourceName)
138
+ return that.translationKeys.no_item_found.replace(
139
+ '%{item}',
140
+ resourceName,
141
+ )
88
142
  },
89
143
  },
90
144
  }
@@ -94,11 +148,22 @@ export default class extends Controller {
94
148
  this.autocompleteTarget.querySelector('button').click()
95
149
  }
96
150
 
151
+ clearValue() {
152
+ this.clearValueTargets.map((e) => e.setAttribute('value', ''))
153
+ this.clearButtonTarget.classList.add('hidden')
154
+ }
155
+
97
156
  connect() {
98
157
  const that = this
99
158
 
100
159
  this.buttonTarget.onclick = () => this.showSearchPanel()
101
160
 
161
+ this.clearValueTargets.forEach((target) => {
162
+ if (target.getAttribute('value') && this.hasClearButtonTarget) {
163
+ this.clearButtonTarget.classList.remove('hidden')
164
+ }
165
+ })
166
+
102
167
  if (this.isGlobalSearch) {
103
168
  Mousetrap.bind(['command+k', 'ctrl+k'], () => this.showSearchPanel())
104
169
  }
@@ -112,12 +177,18 @@ export default class extends Controller {
112
177
  openOnFocus: true,
113
178
  detachedMediaQuery: '',
114
179
  getSources: ({ query }) => {
115
- const endpoint = `${that.searchUrl}?q=${query}`
180
+ const endpoint = that.searchUrl(query)
116
181
 
117
- return that.debouncedFetch(endpoint)
182
+ return that
183
+ .debouncedFetch(endpoint)
118
184
  .then((response) => response.json())
119
185
  .then((data) => Object.keys(data).map((resourceName) => that.addSource(resourceName, data[resourceName])))
120
186
  },
121
187
  })
188
+
189
+ // When using search for belongs-to
190
+ if (this.buttonTarget.dataset.shouldBeDisabled !== 'true') {
191
+ this.buttonTarget.removeAttribute('disabled')
192
+ }
122
193
  }
123
194
  }
@@ -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 %>
@@ -1 +1,3 @@
1
- <%= image_tag '/avo-assets/logo.png', class: 'h-full', title: 'Avo' %>
1
+ <%= link_to root_path, class: 'logo-placeholder h-16 bg-white p-2 flex justify-center' do %>
2
+ <%= image_tag '/avo-assets/logo.png', class: 'h-full', title: 'Avo' %>
3
+ <% 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>
@@ -1,3 +1,9 @@
1
1
  <% if name.present? %><turbo-frame id="<%= name %>"><% end %>
2
- <%= yield %>
2
+ <%
3
+ # When rendering the frames the flashed content gets lost.
4
+ # By including the alerts partial, the stimulus will pick them up and display them to the user.
5
+ %>
6
+ <%= render partial: 'avo/partials/alerts'if flash.present? if flash.present? %>
7
+
8
+ <%= yield %>
3
9
  <% if name.present? %></turbo-frame><% end %>
@@ -1,8 +1,6 @@
1
1
  <div class="application-sidebar flex h-full bg-white text-white w-56 z-50 border-r border-gray-300">
2
2
  <div class="flex flex-col w-full">
3
- <%= link_to root_path, class: 'logo-placeholder h-16 bg-white p-2 flex justify-center' do %>
4
- <%= render partial: "avo/partials/logo" %>
5
- <% end %>
3
+ <%= render partial: "avo/partials/logo" %>
6
4
 
7
5
  <div class="flex-1 flex flex-col justify-between">
8
6
  <div class="tools py-4">
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
@@ -54,11 +58,15 @@ FactoryBot.define do
54
58
  type { "Spouse" }
55
59
  end
56
60
 
61
+ factory :fish do
62
+ name { %w[Tilapia Salmon Trout Catfish Pangasius Carp].sample }
63
+ end
64
+
57
65
  factory :course do
58
- name { Faker::Company.name }
66
+ name { Faker::Educator.unique.course_name }
59
67
  end
60
68
 
61
- factory :link do
62
- name { Faker::Internet.url }
69
+ factory :course_link, class: "Course::Link" do
70
+ link { Faker::Internet.url }
63
71
  end
64
72
  end
@@ -6,7 +6,7 @@ module Avo
6
6
 
7
7
  include ActionView::Helpers::UrlHelper
8
8
 
9
- delegate :view_context, to: 'Avo::App'
9
+ delegate :view_context, to: "Avo::App"
10
10
  delegate :main_app, to: :view_context
11
11
  delegate :avo, to: :view_context
12
12
  delegate :resource_path, to: :view_context
@@ -21,7 +21,7 @@ module Avo
21
21
  class_attribute :title, default: :id
22
22
  class_attribute :description, default: :id
23
23
  class_attribute :search_query, default: nil
24
- class_attribute :search_query_help, default: ''
24
+ class_attribute :search_query_help, default: ""
25
25
  class_attribute :includes, default: []
26
26
  class_attribute :model_class
27
27
  class_attribute :translation_key
@@ -60,7 +60,7 @@ module Avo
60
60
  # This is the search_query scope
61
61
  # This should be removed and passed to the search block
62
62
  def scope
63
- self.query_scope
63
+ query_scope
64
64
  end
65
65
 
66
66
  # This resolves the scope when doing "where" queries (not find queries)
@@ -128,17 +128,19 @@ module Avo
128
128
  end
129
129
  .select do |field|
130
130
  # Strip out the reflection field in index queries with a parent association.
131
- if reflection.present? &&
132
- reflection.options.present? &&
133
- field.respond_to?(:polymorphic_as) &&
134
- field.polymorphic_as.to_s == reflection.options[:as].to_s
135
- next
136
- end
137
- if field.respond_to?(:foreign_key) &&
138
- reflection.present? &&
139
- reflection.respond_to?(:foreign_key) &&
140
- reflection.foreign_key != field.foreign_key
141
- next
131
+ if reflection.present?
132
+ if reflection.options.present? &&
133
+ field.respond_to?(:polymorphic_as) &&
134
+ field.polymorphic_as.to_s == reflection.options[:as].to_s
135
+ next
136
+ end
137
+
138
+ if field.respond_to?(:foreign_key) &&
139
+ reflection.respond_to?(:foreign_key) &&
140
+ reflection.foreign_key != field.foreign_key &&
141
+ @params[:resource_name] == field.resource.model_key
142
+ next
143
+ end
142
144
  end
143
145
 
144
146
  true
@@ -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