avo 4.0.0.beta.42 → 4.0.0.beta.44
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/app/assets/builds/avo/application.css +38 -7
- data/app/assets/builds/avo/application.js +94 -94
- data/app/assets/builds/avo/application.js.map +4 -4
- data/app/assets/stylesheets/css/components/header_menu.css +1 -6
- data/app/assets/stylesheets/css/components/hotkey.css +16 -0
- data/app/assets/stylesheets/css/layout.css +6 -0
- data/app/components/avo/keyboard_shortcuts_component.html.erb +40 -18
- data/app/javascript/js/controllers/keyboard_shortcuts_search_controller.js +78 -0
- data/app/javascript/js/controllers.js +2 -0
- data/lib/avo/fields/belongs_to_field.rb +18 -7
- data/lib/avo/version.rb +1 -1
- metadata +2 -1
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
overflow-clip-margin: 4px;
|
|
11
11
|
|
|
12
12
|
> a {
|
|
13
|
-
@apply text-sm font-medium rounded-md
|
|
13
|
+
@apply text-sm font-medium rounded-md outline-offset-2;
|
|
14
14
|
}
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -18,11 +18,6 @@
|
|
|
18
18
|
@apply whitespace-nowrap shrink-0;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
.header-menu__row > a:focus-visible {
|
|
22
|
-
@apply relative z-10;
|
|
23
|
-
outline-offset: var(--focus-outline-offset-inset);
|
|
24
|
-
}
|
|
25
|
-
|
|
26
21
|
/* The last visible item may be truncated to fill leftover space. */
|
|
27
22
|
.header-menu__row > .header-menu__item--truncated {
|
|
28
23
|
@apply overflow-hidden text-ellipsis min-w-0;
|
|
@@ -1,3 +1,15 @@
|
|
|
1
|
+
.hotkey__search {
|
|
2
|
+
@apply sticky top-0 z-10 relative flex items-center px-6 py-4 bg-primary border-b border-tertiary;
|
|
3
|
+
}
|
|
4
|
+
|
|
5
|
+
.hotkey__search-icon {
|
|
6
|
+
@apply pointer-events-none absolute z-20 size-4 text-content-secondary start-9;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
.hotkey__search-input {
|
|
10
|
+
@apply ps-9;
|
|
11
|
+
}
|
|
12
|
+
|
|
1
13
|
.hotkey__grid {
|
|
2
14
|
@apply p-6;
|
|
3
15
|
|
|
@@ -9,6 +21,10 @@
|
|
|
9
21
|
}
|
|
10
22
|
}
|
|
11
23
|
|
|
24
|
+
.hotkey__empty {
|
|
25
|
+
@apply px-6 pb-8 text-sm text-content-secondary;
|
|
26
|
+
}
|
|
27
|
+
|
|
12
28
|
.hotkey__section {
|
|
13
29
|
@apply min-w-0 mb-10;
|
|
14
30
|
|
|
@@ -307,6 +307,12 @@ button[data-sidebar-toggle-icon] {
|
|
|
307
307
|
--btn-text-color: var(--top-navbar-content-hover);
|
|
308
308
|
background-color: var(--top-navbar-control-background);
|
|
309
309
|
}
|
|
310
|
+
|
|
311
|
+
/* The icons are nested inside `.button__label`, so the button's own
|
|
312
|
+
`items-center` doesn't reach them — center them within the label. */
|
|
313
|
+
.button__label {
|
|
314
|
+
@apply flex items-center;
|
|
315
|
+
}
|
|
310
316
|
}
|
|
311
317
|
|
|
312
318
|
/* Default hidden state - LTR: sidebar slides in from left.
|
|
@@ -4,25 +4,47 @@
|
|
|
4
4
|
width: :xl,
|
|
5
5
|
behavior: :persistent
|
|
6
6
|
) do %>
|
|
7
|
-
<div
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
7
|
+
<div data-controller="keyboard-shortcuts-search">
|
|
8
|
+
<div class="hotkey__search">
|
|
9
|
+
<%= helpers.svg "tabler/outline/search", class: "hotkey__search-icon" %>
|
|
10
|
+
<%= tag.input(
|
|
11
|
+
type: "search",
|
|
12
|
+
class: "hotkey__search-input",
|
|
13
|
+
placeholder: t("avo.search.placeholder"),
|
|
14
|
+
autocomplete: "off",
|
|
15
|
+
spellcheck: "false",
|
|
16
|
+
aria: {label: "Search keyboard shortcuts"},
|
|
17
|
+
data: {
|
|
18
|
+
"keyboard-shortcuts-search-target": "input",
|
|
19
|
+
action: "input->keyboard-shortcuts-search#filter keydown.esc->keyboard-shortcuts-search#clearOnEscape"
|
|
20
|
+
}
|
|
21
|
+
) %>
|
|
22
|
+
</div>
|
|
13
23
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
24
|
+
<div class="hotkey__grid">
|
|
25
|
+
<% sections.each do |section| %>
|
|
26
|
+
<section class="hotkey__section" aria-labelledby="<%= section[:id] %>" data-keyboard-shortcuts-search-target="section">
|
|
27
|
+
<h3 class="hotkey__section-title" id="<%= section[:id] %>">
|
|
28
|
+
<%= section[:title] %>
|
|
29
|
+
</h3>
|
|
18
30
|
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
31
|
+
<ul class="hotkey__section-body">
|
|
32
|
+
<% section[:shortcuts].each do |shortcut| %>
|
|
33
|
+
<li class="hotkey__row" data-keyboard-shortcuts-search-target="row">
|
|
34
|
+
<span class="hotkey__action"><%= shortcut[:action] %></span>
|
|
35
|
+
|
|
36
|
+
<%= tag.span class: "hotkey__keys", **(shortcut[:keys_aria_label].present? ? {aria: {label: shortcut[:keys_aria_label]}} : {}) do %>
|
|
37
|
+
<%= render_shortcut_keys(shortcut) %>
|
|
38
|
+
<% end %>
|
|
39
|
+
</li>
|
|
40
|
+
<% end %>
|
|
41
|
+
</ul>
|
|
42
|
+
</section>
|
|
43
|
+
<% end %>
|
|
44
|
+
</div>
|
|
45
|
+
|
|
46
|
+
<p class="hotkey__empty" data-keyboard-shortcuts-search-target="empty" hidden>
|
|
47
|
+
<%= t("avo.global_search.empty_state.no_results_found") %>
|
|
48
|
+
</p>
|
|
27
49
|
</div>
|
|
28
50
|
<% end %>
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
import { Controller } from '@hotwired/stimulus'
|
|
2
|
+
|
|
3
|
+
// Filters the rows inside the keyboard shortcuts modal as the user types.
|
|
4
|
+
// Hides non-matching rows and any section left empty, and reveals an empty
|
|
5
|
+
// state when nothing matches. Focus is handed to the input whenever the modal
|
|
6
|
+
// (a popover) opens, and the query is reset on close so each visit starts fresh.
|
|
7
|
+
export default class extends Controller {
|
|
8
|
+
static targets = ['input', 'section', 'row', 'empty']
|
|
9
|
+
|
|
10
|
+
connect() {
|
|
11
|
+
this.modal = this.element.closest('.modal')
|
|
12
|
+
if (this.modal) {
|
|
13
|
+
this.handleToggle = this.onModalToggle.bind(this)
|
|
14
|
+
this.modal.addEventListener('toggle', this.handleToggle)
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
disconnect() {
|
|
19
|
+
if (this.modal) this.modal.removeEventListener('toggle', this.handleToggle)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
onModalToggle(event) {
|
|
23
|
+
if (event.newState === 'open') {
|
|
24
|
+
this.reset()
|
|
25
|
+
// Defer focus until the popover is painted and focusable.
|
|
26
|
+
requestAnimationFrame(() => this.inputTarget.focus())
|
|
27
|
+
} else {
|
|
28
|
+
this.reset()
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
filter() {
|
|
33
|
+
const query = this.inputTarget.value.trim().toLowerCase()
|
|
34
|
+
|
|
35
|
+
let anyVisible = false
|
|
36
|
+
|
|
37
|
+
this.sectionTargets.forEach((section) => {
|
|
38
|
+
let sectionHasMatch = false
|
|
39
|
+
|
|
40
|
+
this.rowsFor(section).forEach((row) => {
|
|
41
|
+
const match = query === '' || this.searchText(row).includes(query)
|
|
42
|
+
row.hidden = !match
|
|
43
|
+
if (match) sectionHasMatch = true
|
|
44
|
+
})
|
|
45
|
+
|
|
46
|
+
section.hidden = !sectionHasMatch
|
|
47
|
+
if (sectionHasMatch) anyVisible = true
|
|
48
|
+
})
|
|
49
|
+
|
|
50
|
+
if (this.hasEmptyTarget) this.emptyTarget.hidden = anyVisible || query === ''
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Escape clears a non-empty query before the modal handles it as a close.
|
|
54
|
+
clearOnEscape(event) {
|
|
55
|
+
if (this.inputTarget.value === '') return
|
|
56
|
+
|
|
57
|
+
event.stopPropagation()
|
|
58
|
+
this.reset()
|
|
59
|
+
this.inputTarget.focus()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
reset() {
|
|
63
|
+
this.inputTarget.value = ''
|
|
64
|
+
this.filter()
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
rowsFor(section) {
|
|
68
|
+
return this.rowTargets.filter((row) => section.contains(row))
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
searchText(row) {
|
|
72
|
+
if (!row.dataset.searchText) {
|
|
73
|
+
row.dataset.searchText = row.textContent.replace(/\s+/g, ' ').trim().toLowerCase()
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
return row.dataset.searchText
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -30,6 +30,7 @@ import IndexRowNavigatorController from './controllers/index_row_navigator_contr
|
|
|
30
30
|
import InputAutofocusController from './controllers/input_autofocus_controller'
|
|
31
31
|
import ItemSelectAllController from './controllers/item_select_all_controller'
|
|
32
32
|
import ItemSelectorController from './controllers/item_selector_controller'
|
|
33
|
+
import KeyboardShortcutsSearchController from './controllers/keyboard_shortcuts_search_controller'
|
|
33
34
|
import KeyValueController from './controllers/fields/key_value_controller'
|
|
34
35
|
import LoadingButtonController from './controllers/loading_button_controller'
|
|
35
36
|
import MapDarkModeController from './controllers/map_dark_mode_controller'
|
|
@@ -90,6 +91,7 @@ application.register('header-menu', HeaderMenuController)
|
|
|
90
91
|
application.register('hidden-input', HiddenInputController)
|
|
91
92
|
application.register('index-row-navigator', IndexRowNavigatorController)
|
|
92
93
|
application.register('input-autofocus', InputAutofocusController)
|
|
94
|
+
application.register('keyboard-shortcuts-search', KeyboardShortcutsSearchController)
|
|
93
95
|
application.register('item-select-all', ItemSelectAllController)
|
|
94
96
|
application.register('item-selector', ItemSelectorController)
|
|
95
97
|
application.register('loading-button', LoadingButtonController)
|
|
@@ -119,11 +119,7 @@ module Avo
|
|
|
119
119
|
resource = target_resource
|
|
120
120
|
resource = Avo.resource_manager.get_resource_by_model_class model if model.present?
|
|
121
121
|
|
|
122
|
-
query = resource
|
|
123
|
-
|
|
124
|
-
if attach_scope.present?
|
|
125
|
-
query = Avo::ExecutionContext.new(target: attach_scope, query: query, parent: get_record).handle
|
|
126
|
-
end
|
|
122
|
+
query = scoped_target_query(resource, get_record)
|
|
127
123
|
|
|
128
124
|
query.all.limit(Avo.configuration.associations_lookup_list_limit).map do |record|
|
|
129
125
|
[resource.new(record: record).record_title, record.to_param]
|
|
@@ -212,12 +208,18 @@ module Avo
|
|
|
212
208
|
if valid_model_class.blank? || id_from_param.blank?
|
|
213
209
|
record.send(:"#{polymorphic_as}_id=", nil)
|
|
214
210
|
else
|
|
215
|
-
|
|
211
|
+
target = target_resource(record:, polymorphic_model_class: value.safe_constantize)
|
|
212
|
+
record_id = target.find_record(id_from_param, query: scoped_target_query(target, record, for_find: true)).id
|
|
216
213
|
|
|
217
214
|
record.send(:"#{polymorphic_as}_id=", record_id)
|
|
218
215
|
end
|
|
219
216
|
else
|
|
220
|
-
|
|
217
|
+
target = target_resource(record:)
|
|
218
|
+
record_id = if value.blank?
|
|
219
|
+
value
|
|
220
|
+
else
|
|
221
|
+
target.find_record(value, query: scoped_target_query(target, record, for_find: true)).send(reflection.association_primary_key)
|
|
222
|
+
end
|
|
221
223
|
|
|
222
224
|
record.send(:"#{key}=", record_id)
|
|
223
225
|
end
|
|
@@ -325,6 +327,15 @@ module Avo
|
|
|
325
327
|
|
|
326
328
|
private
|
|
327
329
|
|
|
330
|
+
def scoped_target_query(resource, parent_record, for_find: false)
|
|
331
|
+
resource_class = resource.is_a?(Class) ? resource : resource.class
|
|
332
|
+
query = for_find ? resource_class.find_scope : resource_class.query_scope
|
|
333
|
+
|
|
334
|
+
return query if attach_scope.blank?
|
|
335
|
+
|
|
336
|
+
Avo::ExecutionContext.new(target: attach_scope, query: query, parent: parent_record).handle
|
|
337
|
+
end
|
|
338
|
+
|
|
328
339
|
def get_model_class(record)
|
|
329
340
|
if record.nil?
|
|
330
341
|
@resource.model_class
|
data/lib/avo/version.rb
CHANGED
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: avo
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 4.0.0.beta.
|
|
4
|
+
version: 4.0.0.beta.44
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Adrian Marin
|
|
@@ -783,6 +783,7 @@ files:
|
|
|
783
783
|
- app/javascript/js/controllers/input_autofocus_controller.js
|
|
784
784
|
- app/javascript/js/controllers/item_select_all_controller.js
|
|
785
785
|
- app/javascript/js/controllers/item_selector_controller.js
|
|
786
|
+
- app/javascript/js/controllers/keyboard_shortcuts_search_controller.js
|
|
786
787
|
- app/javascript/js/controllers/loading_button_controller.js
|
|
787
788
|
- app/javascript/js/controllers/map_dark_mode_controller.js
|
|
788
789
|
- app/javascript/js/controllers/media_library_attach_controller.js
|