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.
- checksums.yaml +4 -4
- data/Gemfile +3 -1
- data/Gemfile.lock +61 -61
- data/README.md +1 -1
- data/app/assets/svgs/x.svg +3 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +29 -0
- data/app/components/avo/fields/belongs_to_field/autocomplete_component.rb +77 -0
- data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +73 -46
- data/app/components/avo/fields/belongs_to_field/edit_component.rb +37 -0
- data/app/components/avo/navigation_link_component.html.erb +1 -1
- data/app/components/avo/navigation_link_component.rb +2 -1
- data/app/components/avo/views/resource_index_component.html.erb +1 -1
- data/app/controllers/avo/application_controller.rb +16 -14
- data/app/controllers/avo/base_controller.rb +1 -4
- 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/fields/date_field_controller.js +10 -2
- data/app/javascript/js/controllers/item_selector_controller.js +29 -19
- data/app/javascript/js/controllers/search_controller.js +88 -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/_logo.html.erb +3 -1
- data/app/views/avo/partials/_resource_search.html.erb +0 -1
- data/app/views/avo/partials/_turbo_frame_wrap.html.erb +7 -1
- data/app/views/avo/sidebar/_sidebar.html.erb +1 -3
- data/db/factories.rb +11 -3
- data/lib/avo/base_resource.rb +16 -14
- 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/fields/key_value_field.rb +28 -8
- 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 +2 -0
- data/lib/generators/avo/templates/locales/avo.nb-NO.yml +1 -0
- data/lib/generators/avo/templates/locales/avo.pt-BR.yml +1 -0
- data/lib/generators/avo/templates/locales/avo.ro.yml +1 -0
- data/public/avo-assets/avo.css +20 -4
- data/public/avo-assets/avo.js +330 -237
- 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
@@ -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
|
|
@@ -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
|
}
|
@@ -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
|
-
|
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(
|
36
|
-
|
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(
|
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
|
-
|
63
|
-
.
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
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
|
-
|
73
|
-
.
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
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 = [
|
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,50 @@ 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
|
+
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(
|
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(
|
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 =
|
180
|
+
const endpoint = that.searchUrl(query)
|
116
181
|
|
117
|
-
return that
|
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"
|
@@ -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
|
-
|
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
|
-
<%=
|
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::
|
66
|
+
name { Faker::Educator.unique.course_name }
|
59
67
|
end
|
60
68
|
|
61
|
-
factory :
|
62
|
-
|
69
|
+
factory :course_link, class: "Course::Link" do
|
70
|
+
link { Faker::Internet.url }
|
63
71
|
end
|
64
72
|
end
|
data/lib/avo/base_resource.rb
CHANGED
@@ -6,7 +6,7 @@ module Avo
|
|
6
6
|
|
7
7
|
include ActionView::Helpers::UrlHelper
|
8
8
|
|
9
|
-
delegate :view_context, to:
|
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
|
-
|
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
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
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
|