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.
@@ -10,7 +10,7 @@
10
10
  overflow-clip-margin: 4px;
11
11
 
12
12
  > a {
13
- @apply text-sm font-medium rounded-md px-2 py-1;
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 class="hotkey__grid">
8
- <% sections.each do |section| %>
9
- <section class="hotkey__section" aria-labelledby="<%= section[:id] %>">
10
- <h3 class="hotkey__section-title" id="<%= section[:id] %>">
11
- <%= section[:title] %>
12
- </h3>
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
- <ul class="hotkey__section-body">
15
- <% section[:shortcuts].each do |shortcut| %>
16
- <li class="hotkey__row">
17
- <span class="hotkey__action"><%= shortcut[:action] %></span>
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
- <%= tag.span class: "hotkey__keys", **(shortcut[:keys_aria_label].present? ? {aria: {label: shortcut[:keys_aria_label]}} : {}) do %>
20
- <%= render_shortcut_keys(shortcut) %>
21
- <% end %>
22
- </li>
23
- <% end %>
24
- </ul>
25
- </section>
26
- <% end %>
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.query_scope
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
- record_id = target_resource(record:, polymorphic_model_class: value.safe_constantize).find_record(id_from_param).id
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
- record_id = value.blank? ? value : target_resource(record:).find_record(value).send(reflection.association_primary_key)
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
@@ -1,3 +1,3 @@
1
1
  module Avo
2
- VERSION = "4.0.0.beta.42" unless const_defined?(:VERSION)
2
+ VERSION = "4.0.0.beta.44" unless const_defined?(:VERSION)
3
3
  end
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.42
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