avo 3.1.3 → 3.1.4

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 (35) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/components/avo/button_component.rb +1 -1
  4. data/app/components/avo/fields/edit_component.rb +2 -0
  5. data/app/components/avo/fields/has_many_field/show_component.html.erb +2 -2
  6. data/app/components/avo/fields/has_many_field/show_component.rb +7 -0
  7. data/app/components/avo/fields/index_component.rb +3 -1
  8. data/app/components/avo/fields/show_component.rb +3 -1
  9. data/app/components/avo/items/switcher_component.rb +1 -1
  10. data/app/components/avo/tab_group_component.html.erb +10 -43
  11. data/app/components/avo/tab_group_component.rb +20 -2
  12. data/app/components/avo/tab_switcher_component.html.erb +14 -12
  13. data/app/components/avo/tab_switcher_component.rb +12 -10
  14. data/app/controllers/avo/base_controller.rb +3 -3
  15. data/app/javascript/avo.base.js +4 -0
  16. data/app/javascript/js/controllers/filter_controller.js +22 -11
  17. data/app/javascript/js/controllers/tabs_controller.js +42 -46
  18. data/app/javascript/js/local-storage-service.js +19 -0
  19. data/app/views/avo/home/_actions.html.erb +1 -1
  20. data/app/views/avo/home/_dashboards.html.erb +1 -1
  21. data/app/views/avo/home/_filters.html.erb +1 -1
  22. data/app/views/avo/home/_resources.html.erb +1 -1
  23. data/lib/avo/base_resource.rb +1 -1
  24. data/lib/avo/concerns/has_items.rb +17 -10
  25. data/lib/avo/filters/base_filter.rb +3 -3
  26. data/lib/avo/resources/items/holder.rb +2 -2
  27. data/lib/avo/resources/items/tab.rb +5 -0
  28. data/lib/avo/resources/items/tab_group.rb +17 -3
  29. data/lib/avo/resources/resource_manager.rb +1 -1
  30. data/lib/avo/test_helpers.rb +2 -2
  31. data/lib/avo/version.rb +1 -1
  32. data/public/avo-assets/avo.base.css +8 -0
  33. data/public/avo-assets/avo.base.js +124 -124
  34. data/public/avo-assets/avo.base.js.map +3 -3
  35. metadata +2 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 58201ebf40ea090bc7ff03e588ae9dba48c913c4d39b16eaa4d0068a99c45211
4
- data.tar.gz: 2c3efea520399494770be31f454086cc4ea6aa24eaa16c735099ef336e059e8d
3
+ metadata.gz: 7fc615ce5edcbf9325987f90d37495c71f641b241fe34d5da9f9fae94018dd94
4
+ data.tar.gz: 03fae5887aa0b9c0371705d30221cb07f9498ff0d3499f1dd35474fcd449c7f3
5
5
  SHA512:
6
- metadata.gz: 5ab96094a881dea361ab8c88817030bf277dc737e70f5f273da59545f1db19e6145aaa14877db6bc6b8aa91347df5c2099a1c57183dc52f6ed10dd896831f852
7
- data.tar.gz: 404d1af6a00033fc33b3e242fb5e10a1c9a66a0b79771615586466848c6022a3a495ca6ec3e890d394f8c3f5c02c28817c30f30e6563dd443aa1cf0cdd7a7084
6
+ metadata.gz: 504866933b84f2a3969ab026260f8213327a5448bc2f9c50bafef4c45c7de2513373cf1c101e240700f1a3398560c174b530e47caf17621355c6329a3d322f2f
7
+ data.tar.gz: be6dedf903e9439795adf0da9a278458ad2b53d4841447c00334b42d9cb6a561f62f987ccd687528d9f6fbd05ffae02f1c8a795a8140f027c18c406091c65f65
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (3.1.3)
4
+ avo (3.1.4)
5
5
  actionview (>= 6.1)
6
6
  active_link_to
7
7
  activerecord (>= 6.1)
@@ -78,7 +78,7 @@ class Avo::ButtonComponent < ViewComponent::Base
78
78
  result += helpers.svg(@icon, class: icon_classes) if @icon.present?
79
79
 
80
80
  if is_not_icon? && content.present?
81
- result += "<span>#{content}</span>"
81
+ result += content
82
82
  end
83
83
 
84
84
  result.html_safe
@@ -7,6 +7,7 @@ class Avo::Fields::EditComponent < ViewComponent::Base
7
7
  attr_reader :field
8
8
  attr_reader :form
9
9
  attr_reader :index
10
+ attr_reader :kwargs
10
11
  attr_reader :multiple
11
12
  attr_reader :resource
12
13
  attr_reader :stacked
@@ -17,6 +18,7 @@ class Avo::Fields::EditComponent < ViewComponent::Base
17
18
  @field = field
18
19
  @form = form
19
20
  @index = index
21
+ @kwargs = kwargs
20
22
  @multiple = multiple
21
23
  @resource = resource
22
24
  @stacked = stacked
@@ -1,3 +1,3 @@
1
- <turbo-frame id="<%= @field.turbo_frame %>" src="<%= @field.frame_url %>" target="_top" class="block">
1
+ <%= turbo_frame_tag @field.turbo_frame, src: @field.frame_url, loading: loading, target: :_top, class: "block" do %>
2
2
  <%= render(Avo::LoadingComponent.new(title: @field.plural_name)) %>
3
- </turbo-frame>
3
+ <% end %>
@@ -1,4 +1,11 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Fields::HasManyField::ShowComponent < Avo::Fields::ShowComponent
4
+ include Turbo::FramesHelper
5
+
6
+ def turbo_frame_loading = kwargs[:turbo_frame_loading]
7
+
8
+ def loading
9
+ turbo_frame_loading || params[:turbo_frame_loading] || "eager"
10
+ end
4
11
  end
@@ -4,13 +4,15 @@ class Avo::Fields::IndexComponent < Avo::BaseComponent
4
4
  include Avo::ResourcesHelper
5
5
 
6
6
  attr_reader :field
7
+ attr_reader :kwargs
7
8
  attr_reader :parent_resource
8
9
  attr_reader :view
9
10
 
10
- def initialize(field: nil, resource: nil, reflection: nil, index: 0, parent_record: nil, parent_resource: nil)
11
+ def initialize(field: nil, resource: nil, reflection: nil, index: 0, parent_record: nil, parent_resource: nil, **kwargs)
11
12
  @field = field
12
13
  @resource = resource
13
14
  @index = index
15
+ @kwargs = kwargs
14
16
  @parent_record = parent_record
15
17
  @parent_resource = parent_resource
16
18
  @view = Avo::ViewInquirer.new("index")
@@ -6,18 +6,20 @@ class Avo::Fields::ShowComponent < ViewComponent::Base
6
6
  attr_reader :compact
7
7
  attr_reader :field
8
8
  attr_reader :index
9
+ attr_reader :kwargs
9
10
  attr_reader :resource
10
11
  attr_reader :stacked
11
12
  attr_reader :short
12
13
  attr_reader :view
13
14
 
14
- def initialize(field: nil, resource: nil, index: 0, form: nil, compact: false, short: false, stacked: nil)
15
+ def initialize(field: nil, resource: nil, index: 0, form: nil, compact: false, short: false, stacked: nil, **kwargs)
15
16
  @compact = compact
16
17
  @field = field
17
18
  @index = index
18
19
  @resource = resource
19
20
  @stacked = stacked
20
21
  @short = short
22
+ @kwargs = kwargs
21
23
  @view = Avo::ViewInquirer.new("show")
22
24
  end
23
25
 
@@ -58,7 +58,7 @@ class Avo::Items::SwitcherComponent < Avo::BaseComponent
58
58
 
59
59
  def field_component
60
60
  final_item = item.dup.hydrate(resource: @resource, record: @resource.record, user: resource.user, view: view)
61
- final_item.component_for_view(@view).new(field: final_item, resource: @resource, index: index, form: form, **@field_component_extra_args)
61
+ final_item.component_for_view(@view).new(field: final_item, resource: @resource, index: index, form: form, turbo_frame_loading: :lazy, **@field_component_extra_args)
62
62
  end
63
63
 
64
64
  def panel_component
@@ -4,53 +4,20 @@
4
4
  index: index,
5
5
  controller: "tabs",
6
6
  tabs_view_value: view,
7
- tabs_active_tab_value: active_tab_name
7
+ tabs_group_id_value: group.id,
8
+ tabs_active_tab_value: active_tab_name,
9
+ tabs_resource_name_value: resource.underscore_name
8
10
  } do %>
9
11
  <% visible_tabs.each_with_index do |tab, index| %>
10
- <%
11
- args = {
12
- # Hide the turbo frames that aren't in the current tab
13
- # This way we can lazy load the un-selected tabs on the show view
14
- class: "block #{'hidden' unless tab.name == active_tab_name}",
15
- data: {
16
- # Add a marker to know if we already loaded a turbo frame
17
- loaded: tab.name == active_tab_name,
18
- tabs_target: :tabPanel,
19
- tab_id: tab.name,
20
- }
21
- }
22
-
23
- is_current_tab = active_tab_name.to_s == tab.name.to_s
24
-
25
- # On edit screens we want to load each tab because we wnst the DOM to have the fields present on form submission.
26
- # If you have a field which is in the second tab and it's required, the form submission will fail because the required field is not in view, and we don't want that.
27
- # We also want to load the current tab
28
- should_lazy_load = if @view.in?(%w[edit new update create])
29
- false
30
- else
31
- !is_current_tab
32
- end
33
-
34
- if should_lazy_load
35
- args[:src] = helpers.resource_path(resource: resource, record: resource.record, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id)
36
- args[:loading] = :lazy
37
- end
38
- %>
39
- <%= turbo_frame_tag tab.turbo_frame_id(parent: @group), **args do %>
12
+ <%= content_tag :div, **args(tab) do %>
40
13
  <div class="border rounded-lg p-2 -mx-2 -my-2 lg:p-4 lg:-mx-4 lg:-my-4 space-y-4">
41
- <%= render Avo::TabSwitcherComponent.new resource: resource, current_tab: tab, group: group, active_tab_name: active_tab_name, view: view %>
42
- <% if should_lazy_load %>
43
- <div class="block w-full">
44
- <%= render Avo::LoadingComponent.new title: "#{tab.name}" %>
14
+ <%= render Avo::TabSwitcherComponent.new resource: resource, current_tab: visible_tabs.first, group: group, active_tab_name: tab.name, view: view %>
15
+ <% if !tab.is_empty? %>
16
+ <div class="space-y-12">
17
+ <% tab.visible_items.each do |item| %>
18
+ <%= render Avo::Items::SwitcherComponent.new resource: resource, item: item, index: index, form: form, view: @view %>
19
+ <% end %>
45
20
  </div>
46
- <% else %>
47
- <% if !tab.is_empty? %>
48
- <div class="space-y-12">
49
- <% tab.visible_items.each do |item| %>
50
- <%= render Avo::Items::SwitcherComponent.new resource: resource, item: item, index: index, form: form, view: @view %>
51
- <% end %>
52
- </div>
53
- <% end %>
54
21
  <% end %>
55
22
  </div>
56
23
  <% end %>
@@ -26,12 +26,16 @@ class Avo::TabGroupComponent < Avo::BaseComponent
26
26
  visible_tabs.present?
27
27
  end
28
28
 
29
+ def group_param
30
+ group.id
31
+ end
32
+
29
33
  def active_tab_name
30
- params[:active_tab_name] || group.visible_items&.first&.name
34
+ CGI.unescape(params[group_param] || group.visible_items&.first&.name)
31
35
  end
32
36
 
33
37
  def tabs
34
- @group.items.map do |tab|
38
+ @group.visible_items.map do |tab|
35
39
  tab.hydrate(view: view)
36
40
  end
37
41
  end
@@ -49,4 +53,18 @@ class Avo::TabGroupComponent < Avo::BaseComponent
49
53
  tab.name.to_s == active_tab_name.to_s
50
54
  end
51
55
  end
56
+
57
+ def args(tab)
58
+ {
59
+ # Hide the turbo frames that aren't in the current tab
60
+ # This way we can lazy load the un-selected tabs on the show view
61
+ class: "block #{'hidden' unless tab.name == active_tab_name}",
62
+ data: {
63
+ # Add a marker to know if we already loaded a turbo frame
64
+ loaded: tab.name == active_tab_name,
65
+ tabs_target: :tabPanel,
66
+ tab_id: tab.name,
67
+ }
68
+ }
69
+ end
52
70
  end
@@ -1,17 +1,19 @@
1
- <div class="flex flex-wrap gap-2 p-2 <%= white_panel_classes %>" data-target="tab-switcher">
1
+ <div class="flex flex-wrap gap-2 p-2 <%= white_panel_classes %>" data-tabs-target="tabSwitcher">
2
2
  <% group.visible_items.each do |tab| %>
3
3
  <%= a_link tab_path(tab),
4
- color: selected?(tab) ? :primary : :gray,
5
- style: selected?(tab) ? :outline : :text,
6
- size: :sm,
7
- class: selected?(tab) ? "z-20 bg-primary-100" : "",
8
- title: tab.description,
9
- data: {
10
- tippy: tab.description.present? ? 'tooltip' : '',
11
- selected: selected?(tab),
12
- action: 'click->tabs#changeTab',
13
- tabs_id_param: tab.name
14
- } do %>
4
+ color: current_one?(tab) ? :primary : :gray,
5
+ style: current_one?(tab) ? :outline : :text,
6
+ size: :sm,
7
+ class: current_one?(tab) ? "z-20 bg-primary-100" : "",
8
+ title: tab.description,
9
+ data: {
10
+ tippy: tab.description.present? ? 'tooltip' : '',
11
+ selected: current_one?(tab),
12
+ action: 'click->tabs#changeTab',
13
+ tabs_tab_name_param: tab.name,
14
+ tabs_group_id_param: group.to_param,
15
+ tabs_resource_name_param: resource.underscore_name,
16
+ } do %>
15
17
  <%= tab.name %>
16
18
  <% end %>
17
19
  <% end %>
@@ -9,6 +9,7 @@ class Avo::TabSwitcherComponent < Avo::BaseComponent
9
9
  attr_reader :current_tab
10
10
  attr_reader :tabs
11
11
  attr_reader :view
12
+ attr_reader :resource
12
13
 
13
14
  delegate :white_panel_classes, to: :helpers
14
15
 
@@ -40,17 +41,18 @@ class Avo::TabSwitcherComponent < Avo::BaseComponent
40
41
  @view.in?(%w[new create])
41
42
  end
42
43
 
43
- def is_initial_load?
44
- params[:active_tab_name].blank?
44
+ # We'll mark the tab as selected if it's the current one
45
+ def current_one?(tab)
46
+ tab.name == active_tab_name
45
47
  end
46
48
 
47
- # On initial load we want that each tab button to be the selected one.
48
- # We do that so we don't get the wrongly selected item for a quick brief when first switching from one panel to another.
49
- def selected?(tab)
50
- if is_initial_load?
51
- current_tab.name.to_s == tab.name.to_s
52
- else
53
- tab.name.to_s == active_tab_name.to_s
54
- end
49
+ private
50
+
51
+ def group_param
52
+ "tab-group_#{group.id}"
53
+ end
54
+
55
+ def tab_param_missing?
56
+ params[group_param].blank?
55
57
  end
56
58
  end
@@ -357,12 +357,12 @@ module Avo
357
357
  def set_applied_filters
358
358
  reset_filters if params[:reset_filter]
359
359
 
360
- @applied_filters = Avo::Filters::BaseFilter.decode_filters(fetch_filters)
360
+ return @applied_filters = {} if (fetched_filters = fetch_filters).blank?
361
+
362
+ @applied_filters = Avo::Filters::BaseFilter.decode_filters(fetched_filters)
361
363
 
362
364
  # Some filters react to others and will have to be merged into this
363
365
  @applied_filters = @applied_filters.merge reactive_filters
364
- rescue
365
- @applied_filters = {}
366
366
  end
367
367
 
368
368
  def reactive_filters
@@ -8,8 +8,12 @@ import { Turbo } from '@hotwired/turbo-rails'
8
8
  import Rails from '@rails/ujs'
9
9
  import tippy from 'tippy.js'
10
10
 
11
+ import { LocalStorageService } from './js/local-storage-service'
12
+
11
13
  import 'chartkick/chart.js/chart.esm'
12
14
 
15
+ window.Avo.localStorage = new LocalStorageService()
16
+
13
17
  import './js/active-storage'
14
18
  import './js/controllers'
15
19
  import './js/custom-stream-actions'
@@ -25,17 +25,28 @@ export default class extends Controller {
25
25
  return param
26
26
  }
27
27
 
28
- b64EncodeUnicode(str) {
29
- // first we use encodeURIComponent to get percent-encoded UTF-8,
30
- // then we convert the percent encodings into raw bytes which
31
- // can be fed into btoa.
32
- return btoa(encodeURIComponent(str).replace(/%([0-9A-F]{2})/g,
33
- (match, p1) => String.fromCharCode(`0x${p1}`)))
28
+ decode(filters) {
29
+ return JSON.parse(
30
+ new TextDecoder().decode(
31
+ Uint8Array.from(
32
+ atob(
33
+ decodeURIComponent(filters),
34
+ ), (m) => m.codePointAt(0),
35
+ ),
36
+ ),
37
+ )
34
38
  }
35
39
 
36
- b64DecodeUnicode(str) {
37
- // Going backwards: from bytestream, to percent-encoding, to original string.
38
- return decodeURIComponent(atob(str).split('').map((c) => `%${(`00${c.charCodeAt(0).toString(16)}`).slice(-2)}`).join(''))
40
+ encode(filtered) {
41
+ return encodeURIComponent(
42
+ btoa(
43
+ String.fromCodePoint(
44
+ ...new TextEncoder().encode(
45
+ JSON.stringify(filtered),
46
+ ),
47
+ ),
48
+ ),
49
+ )
39
50
  }
40
51
 
41
52
  changeFilter() {
@@ -47,7 +58,7 @@ export default class extends Controller {
47
58
 
48
59
  // Decode the filters
49
60
  if (filters) {
50
- filters = JSON.parse(this.b64DecodeUnicode(filters))
61
+ filters = this.decode(filters)
51
62
  } else {
52
63
  filters = {}
53
64
  }
@@ -68,7 +79,7 @@ export default class extends Controller {
68
79
 
69
80
  // Encode the filters and their values
70
81
  if (filtered && Object.keys(filtered).length > 0) {
71
- encodedFilters = this.b64EncodeUnicode(JSON.stringify(filtered))
82
+ encodedFilters = this.encode(filtered)
72
83
  }
73
84
 
74
85
  this.navigateToURLWithFilters(encodedFilters)
@@ -1,74 +1,70 @@
1
1
  import { Controller } from '@hotwired/stimulus'
2
- import { castBoolean } from '../helpers/cast_boolean'
3
2
 
4
3
  export default class extends Controller {
5
- static targets = ['tabPanel'];
4
+ static targets = ['tabPanel']
6
5
 
7
6
  static values = {
8
7
  view: String,
9
8
  activeTab: String,
10
- };
11
-
12
- get currentTab() {
13
- return this.tabPanelTargets.find(
14
- (element) => element.dataset.tabId === this.activeTabValue,
15
- )
9
+ groupId: String,
10
+ resourceName: String,
16
11
  }
17
12
 
18
- targetTabPanel(id) {
19
- return this.tabPanelTargets.find((element) => element.dataset.tabId === id)
13
+ connect() {
14
+ this.selectCurrentTab()
20
15
  }
21
16
 
22
- async changeTab(e) {
23
- // Stopping the link execution.
24
- // We're going to reveal a lazy-loaded frame to fulfill the tab change.
25
- e.preventDefault()
17
+ selectCurrentTab() {
18
+ const params = {}
19
+ Array.from(new URL(window.location).searchParams.entries()).forEach(([key, value]) => { params[key] = value })
26
20
 
27
- const { params } = e
28
- const { id } = params
21
+ const key = `resources.${this.resourceNameValue}.tabgroups.${this.groupIdValue}.selectedTab`
29
22
 
30
- await this.setTheTargetPanelHeight(id)
23
+ // LocalStorage value
24
+ const lsValue = window.Avo.localStorage.get(key)
31
25
 
32
- this.hideAllTabs()
33
- this.revealTab(id)
34
- this.markTabLoaded(id)
35
-
36
- this.activeTabValue = id
37
- }
26
+ let groupId = null
38
27
 
39
- /**
40
- * Sets the target container height to the previous panel height so we don't get jerky tab changes.
41
- */
42
- async setTheTargetPanelHeight(id) {
43
- // Ignore this on edit.
44
- // All tabs are loaded beforehand, they have their own height, and the page won't jiggle when the user toggles between them.
45
- if (this.viewValue === 'edit' || this.viewValue === 'new') {
46
- return
28
+ // if this tab group has a param in the address, select it
29
+ if (params[this.groupParam(this.groupIdValue)]) {
30
+ groupId = params[this.groupParam(this.groupIdValue)]
31
+ } else if (lsValue) {
32
+ groupId = lsValue
47
33
  }
48
34
 
49
- // We don't need to add a height to this panel because it was loaded before
50
- if (castBoolean(this.targetTabPanel(id).dataset.loaded)) {
51
- return
35
+ if (this.getTabByName(groupId)) {
36
+ this.hideAllTabs()
37
+ this.revealTabByName(groupId)
52
38
  }
39
+ }
53
40
 
54
- // Get the height of the active panel
55
- const { height } = this.currentTab.getBoundingClientRect()
56
- // Set it to the target panel
57
- this.targetTabPanel(id).style.height = `${height}px`
41
+ getTabByName(id) {
42
+ return this.tabPanelTargets.find((element) => element.dataset.tabId === id)
43
+ }
58
44
 
59
- // Wait until the panel loaded it's content and then remove the forced height
60
- await this.targetTabPanel(id).loaded
61
- this.targetTabPanel(id).style.height = ''
45
+ groupParam(groupId) {
46
+ return encodeURIComponent(`tab-group_${groupId}`)
62
47
  }
63
48
 
64
- // Marking tab as loaded so we know to skip some things the next time the user clicks on it
65
- markTabLoaded(id) {
66
- this.targetTabPanel(id).dataset.loaded = true
49
+ async changeTab(e) {
50
+ e.preventDefault()
51
+ const { params } = e
52
+ const { groupId, tabName, resourceName } = params
53
+ const key = `resources.${resourceName}.tabgroups.${groupId}.selectedTab`
54
+
55
+ const u = new URL(window.location)
56
+ u.searchParams.set(this.groupParam(groupId), encodeURIComponent(tabName))
57
+ window.history.replaceState(null, '', u.pathname + u.search)
58
+
59
+ window.Avo.localStorage.set(key, tabName)
60
+
61
+ this.hideAllTabs()
62
+ this.revealTabByName(tabName)
67
63
  }
68
64
 
69
65
  // We're revealing the new tab that's lazy loaded by Turbo.
70
- revealTab(id) {
71
- this.targetTabPanel(id).classList.remove('hidden')
66
+ revealTabByName(name) {
67
+ this.getTabByName(name).classList.remove('hidden')
72
68
  }
73
69
 
74
70
  hideAllTabs() {
@@ -0,0 +1,19 @@
1
+ export class LocalStorageService {
2
+ prefix = 'avo'
3
+
4
+ prefixedKey(key) {
5
+ return `${this.prefix}.${key}`
6
+ }
7
+
8
+ get(key) {
9
+ return window.localStorage.getItem(this.prefixedKey(key))
10
+ }
11
+
12
+ set(key, value) {
13
+ return window.localStorage.setItem(this.prefixedKey(key), value)
14
+ }
15
+
16
+ remove(key) {
17
+ return window.localStorage.removeItem(this.prefixedKey(key))
18
+ }
19
+ }
@@ -11,6 +11,6 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <a href="https://docs.avohq.io/2.0/actions.html" target="_blank" title="Avo Actions documentation" class="text-bold cursor-pointer block mt-2">Actions in the docs 👉</a>
14
+ <a href="https://docs.avohq.io/3.0/actions.html" target="_blank" title="Avo Actions documentation" class="text-bold cursor-pointer block mt-2">Actions in the docs 👉</a>
15
15
  </div>
16
16
  </div>
@@ -13,7 +13,7 @@
13
13
  </div>
14
14
 
15
15
  <p>
16
- <a href="https://docs.avohq.io/2.0/dashboards.html" target="_blank" title="Avo Dashboards documentation">Docs</a>
16
+ <a href="https://docs.avohq.io/3.0/dashboards.html" target="_blank" title="Avo Dashboards documentation">Docs</a>
17
17
  </p>
18
18
  </div>
19
19
 
@@ -11,6 +11,6 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <a href="https://docs.avohq.io/2.0/filters.html" target="_blank" title="Avo Filters documentation" class="text-bold cursor-pointer block mt-2">Filters in the docs 👉</a>
14
+ <a href="https://docs.avohq.io/3.0/filters.html" target="_blank" title="Avo Filters documentation" class="text-bold cursor-pointer block mt-2">Filters in the docs 👉</a>
15
15
  </div>
16
16
  </div>
@@ -35,7 +35,7 @@
35
35
  <% end %>
36
36
 
37
37
  <p>
38
- <a href="https://docs.avohq.io/2.0/resources.html" target="_blank" title="Avo Resources documentation">Docs</a>
38
+ <a href="https://docs.avohq.io/3.0/resources.html" target="_blank" title="Avo Resources documentation">Docs</a>
39
39
  </p>
40
40
  </div>
41
41
 
@@ -231,7 +231,7 @@ module Avo
231
231
  delegate :singular_name, to: :class
232
232
  delegate :plural_name, to: :class
233
233
  delegate :underscore_name, to: :class
234
- delegate :underscore_name, to: :class
234
+ delegate :to_param, to: :class
235
235
  delegate :find_record, to: :class
236
236
  delegate :model_key, to: :class
237
237
  delegate :tab, to: :items_holder
@@ -275,16 +275,23 @@ module Avo
275
275
  end
276
276
  end
277
277
  .select do |item|
278
- # On location field we can have field coordinates and setters with different names like latitude and longitude
279
- if !item.is_a?(Avo::Fields::LocationField) && !item.is_heading? && view.in?(%w[edit update new create])
280
- if item.respond_to?(:id)
281
- item.resource.record.respond_to?("#{item.id}=")
282
- else
283
- true
284
- end
285
- else
286
- true
287
- end
278
+ # Check if record has the setter method
279
+ # Next if the view is not on forms
280
+ next true if !view.in?(%w[edit update new create])
281
+
282
+ # Skip items that don't have an id
283
+ next true if !item.respond_to?(:id)
284
+
285
+ # Skip tab groups
286
+ # Skip headings
287
+ # Skip location fields
288
+ # On location field we can have field coordinates and setters with different names
289
+ # like latitude and longitude
290
+ next true if item.is_a?(Avo::Resources::Items::TabGroup) ||
291
+ item.is_heading? ||
292
+ item.is_a?(Avo::Fields::LocationField)
293
+
294
+ item.resource.record.respond_to?("#{item.id}=")
288
295
  end
289
296
  .select do |item|
290
297
  # Check if the user is authorized to view it.
@@ -23,8 +23,6 @@ module Avo
23
23
  class << self
24
24
  def decode_filters(filter_params)
25
25
  JSON.parse(Base64.decode64(filter_params))
26
- rescue
27
- {}
28
26
  end
29
27
 
30
28
  def encode_filters(filter_params)
@@ -67,7 +65,9 @@ module Avo
67
65
 
68
66
  # Fetch the applied filters from the params
69
67
  def applied_filters
70
- self.class.decode_filters params[PARAM_KEY]
68
+ return {} if (filters_from_params = params[PARAM_KEY]).blank?
69
+
70
+ self.class.decode_filters filters_from_params
71
71
  end
72
72
 
73
73
  def visible_in_view(resource: nil, parent_resource: nil)
@@ -37,11 +37,11 @@ class Avo::Resources::Items::Holder
37
37
  add_item field_parser.instance
38
38
  end
39
39
 
40
- def tabs(tab = nil, **kwargs, &block)
40
+ def tabs(tab = nil, id: nil, name: nil, **kwargs, &block)
41
41
  if tab.present?
42
42
  add_item tab
43
43
  else
44
- add_item Avo::Resources::Items::TabGroup::Builder.parse_block(parent: @parent, **kwargs, &block)
44
+ add_item Avo::Resources::Items::TabGroup::Builder.parse_block(parent: @parent, id: id, name: name, **kwargs, &block)
45
45
  end
46
46
  end
47
47
 
@@ -24,6 +24,11 @@ class Avo::Resources::Items::Tab
24
24
  Avo::ExecutionContext.new(target: @name).handle
25
25
  end
26
26
 
27
+ def id
28
+ name.to_s.parameterize
29
+ end
30
+ alias_method :to_param, :id
31
+
27
32
  def turbo_frame_id(parent: nil)
28
33
  id = "#{Avo::Resources::Items::Tab.to_s.parameterize} #{name}".parameterize
29
34