avo 2.9.1.pre7 → 2.10.2

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 (75) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +8 -10
  3. data/README.md +4 -0
  4. data/app/assets/stylesheets/css/buttons.css +4 -1
  5. data/app/components/avo/base_component.rb +2 -0
  6. data/app/components/avo/button_component.rb +3 -1
  7. data/app/components/avo/fields/edit_component.rb +5 -0
  8. data/app/components/avo/fields/show_component.rb +1 -1
  9. data/app/components/avo/index/ordering/button_component.rb +3 -5
  10. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  11. data/app/components/avo/index/table_row_component.html.erb +1 -1
  12. data/app/components/avo/item_switcher_component.html.erb +19 -0
  13. data/app/components/avo/item_switcher_component.rb +45 -0
  14. data/app/components/avo/panel_component.html.erb +23 -24
  15. data/app/components/avo/panel_component.rb +8 -5
  16. data/app/components/avo/tab_group_component.html.erb +53 -0
  17. data/app/components/avo/tab_group_component.rb +51 -0
  18. data/app/components/avo/tab_switcher_component.html.erb +21 -0
  19. data/app/components/avo/tab_switcher_component.rb +86 -0
  20. data/app/components/avo/views/resource_edit_component.html.erb +34 -56
  21. data/app/components/avo/views/resource_edit_component.rb +10 -0
  22. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  23. data/app/components/avo/views/resource_index_component.rb +1 -1
  24. data/app/components/avo/views/resource_show_component.html.erb +58 -89
  25. data/app/components/avo/views/resource_show_component.rb +2 -2
  26. data/app/controllers/avo/actions_controller.rb +1 -1
  27. data/app/controllers/avo/application_controller.rb +11 -15
  28. data/app/helpers/avo/application_helper.rb +0 -6
  29. data/app/helpers/avo/url_helpers.rb +1 -1
  30. data/app/javascript/js/controllers/loading_button_controller.js +25 -21
  31. data/app/javascript/js/controllers/tabs_controller.js +9 -3
  32. data/app/javascript/js/controllers.js +2 -0
  33. data/app/views/avo/base/index.html.erb +1 -1
  34. data/app/views/avo/base/show.html.erb +1 -1
  35. data/app/views/avo/cards/show.html.erb +1 -1
  36. data/app/views/avo/debug/index.html.erb +1 -1
  37. data/app/views/avo/home/_actions.html.erb +1 -1
  38. data/app/views/avo/home/_dashboards.html.erb +19 -0
  39. data/app/views/avo/home/_filters.html.erb +1 -1
  40. data/app/views/avo/home/_resources.html.erb +1 -1
  41. data/app/views/avo/home/failed_to_load.html.erb +1 -1
  42. data/app/views/avo/home/index.html.erb +14 -2
  43. data/app/views/avo/partials/_tabs_toggle.html.erb +20 -0
  44. data/app/views/avo/private/design.html.erb +1 -1
  45. data/lib/avo/base_action.rb +2 -19
  46. data/lib/avo/base_resource.rb +0 -94
  47. data/lib/avo/base_resource_tool.rb +3 -1
  48. data/lib/avo/concerns/has_fields.rb +247 -50
  49. data/lib/avo/concerns/has_html_attributes.rb +1 -1
  50. data/lib/avo/concerns/is_resource_item.rb +36 -0
  51. data/lib/avo/dsl/field_parser.rb +83 -0
  52. data/lib/avo/fields/base_field.rb +19 -2
  53. data/lib/avo/fields/field_extensions/visible_in_different_views.rb +18 -1
  54. data/lib/avo/fields/has_base_field.rb +18 -1
  55. data/lib/avo/fields/key_value_field.rb +4 -4
  56. data/lib/avo/grid_collector.rb +6 -3
  57. data/lib/avo/items_holder.rb +68 -0
  58. data/lib/avo/licensing/h_q.rb +10 -0
  59. data/lib/avo/main_panel.rb +3 -0
  60. data/lib/avo/menu/builder.rb +6 -6
  61. data/lib/avo/panel.rb +25 -0
  62. data/lib/avo/panel_builder.rb +23 -0
  63. data/lib/avo/tab.rb +78 -0
  64. data/lib/avo/tab_builder.rb +25 -0
  65. data/lib/avo/tab_group.rb +40 -0
  66. data/lib/avo/tab_group_builder.rb +43 -0
  67. data/lib/avo/version.rb +1 -1
  68. data/lib/generators/avo/templates/resource/controller.tt +2 -0
  69. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  70. data/lib/generators/avo/templates/tool/view.tt +1 -1
  71. data/public/avo-assets/avo.css +27 -3
  72. data/public/avo-assets/avo.js +73 -73
  73. data/public/avo-assets/avo.js.map +3 -3
  74. metadata +22 -5
  75. data/lib/avo/concerns/has_tools.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: d1125e5ae529c104548f0c947d1f878be1fa5aafc821d745df6c7a09077c1cb8
4
- data.tar.gz: c85ba14cf3905f39ba12c688166ee88819c33c8b07af4507accdedfb8463311f
3
+ metadata.gz: 1fcc28841e21f49a9ad7146589f8e280f352d160106bc67539630a819a2fea14
4
+ data.tar.gz: 7d0049458872fa7a81003d1358673b65ff29d38256b6d2d20c8fdaf757b09d8f
5
5
  SHA512:
6
- metadata.gz: 587d36901f4ef5b250200c1903c7e7a8833933e9ff4a1fe565fe79ffef96abc9583eea274bc94e0ffbd62f29c1a567a7f5b758cf934bf04a09284e943cc6203e
7
- data.tar.gz: 2efbbc0b87bff85f640e1a591c4341f39c1588938d3e7553600a069fc1f9d4b6fe79f3723d0c60972bffeee54c3dd4d3946e22871900bec6efcfa42ce652400a
6
+ metadata.gz: aa278bfe2a28f7f441239455246af82da628b9d699e7a583e6da65972305c75e407fce432341b11d21ec5225ff67c8558b6017bb75a1d42e7be42f3653e0d259
7
+ data.tar.gz: 8a6ff3c392ce98703272555aabe01ae005f8df10081c7a1bde90e5a7ef99063144d5d573b4012b31b44d0b0c585334ee69fabf5f4414d1e053c2abaa61fe6ae6
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (2.9.1.pre7)
4
+ avo (2.10.2)
5
5
  active_link_to
6
6
  addressable
7
7
  breadcrumbs_on_rails
@@ -138,7 +138,7 @@ GEM
138
138
  xpath (~> 3.2)
139
139
  chartkick (4.2.0)
140
140
  childprocess (4.1.0)
141
- concurrent-ruby (1.1.9)
141
+ concurrent-ruby (1.1.10)
142
142
  countries (4.2.1)
143
143
  i18n_data (~> 0.15.0)
144
144
  sixarm_ruby_unaccent (~> 1.1)
@@ -219,7 +219,7 @@ GEM
219
219
  listen (3.7.1)
220
220
  rb-fsevent (~> 0.10, >= 0.10.3)
221
221
  rb-inotify (~> 0.9, >= 0.9.10)
222
- loofah (2.14.0)
222
+ loofah (2.18.0)
223
223
  crass (~> 1.0.2)
224
224
  nokogiri (>= 1.5.9)
225
225
  mail (2.7.1)
@@ -238,7 +238,7 @@ GEM
238
238
  mini_magick (4.11.0)
239
239
  mini_mime (1.1.2)
240
240
  mini_portile2 (2.8.0)
241
- minitest (5.15.0)
241
+ minitest (5.16.2)
242
242
  msgpack (1.4.4)
243
243
  multi_xml (0.6.0)
244
244
  net-protocol (0.1.2)
@@ -249,11 +249,9 @@ GEM
249
249
  net-protocol
250
250
  timeout
251
251
  nio4r (2.5.8)
252
- nokogiri (1.13.4)
252
+ nokogiri (1.13.6)
253
253
  mini_portile2 (~> 2.8.0)
254
254
  racc (~> 1.4)
255
- nokogiri (1.13.4-x86_64-linux)
256
- racc (~> 1.4)
257
255
  orm_adapter (0.5.0)
258
256
  pagy (5.10.1)
259
257
  activesupport
@@ -292,7 +290,7 @@ GEM
292
290
  rails-dom-testing (2.0.3)
293
291
  activesupport (>= 4.2.0)
294
292
  nokogiri (>= 1.6)
295
- rails-html-sanitizer (1.4.2)
293
+ rails-html-sanitizer (1.4.3)
296
294
  loofah (~> 2.3)
297
295
  railties (6.1.4.6)
298
296
  actionpack (= 6.1.4.6)
@@ -393,7 +391,7 @@ GEM
393
391
  tzinfo (2.0.4)
394
392
  concurrent-ruby (~> 1.0)
395
393
  unicode-display_width (2.1.0)
396
- view_component (2.49.1)
394
+ view_component (2.57.1)
397
395
  activesupport (>= 5.0.0, < 8.0)
398
396
  method_source (~> 1.0)
399
397
  warden (1.2.9)
@@ -416,7 +414,7 @@ GEM
416
414
  websocket-extensions (0.1.5)
417
415
  xpath (3.2.0)
418
416
  nokogiri (~> 1.8)
419
- zeitwerk (2.5.4)
417
+ zeitwerk (2.6.0)
420
418
 
421
419
  PLATFORMS
422
420
  ruby
data/README.md CHANGED
@@ -69,6 +69,10 @@ Please read [CONTRIBUTING.MD](./CONTRIBUTING.MD)
69
69
 
70
70
  Please read the [UPGRADE_GUIDE.MD](https://docs.avohq.io/2.0/upgrade.html)
71
71
 
72
+ # Release schedule
73
+
74
+ Please read the [RELEASE.MD](./RELEASE.MD)
75
+
72
76
  # ✨ Contributors
73
77
 
74
78
  <a href="https://github.com/avo-hq/avo/graphs/contributors">
@@ -1,8 +1,11 @@
1
1
  .button-group {
2
2
  @apply flex;
3
+
3
4
  .button-component {
5
+ @apply -mr-px;
6
+
4
7
  &:first-child {
5
- @apply rounded-l -mr-px;
8
+ @apply rounded-l;
6
9
  }
7
10
 
8
11
  &:last-child {
@@ -1,6 +1,8 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::BaseComponent < ViewComponent::Base
4
+ include Turbo::FramesHelper
5
+
4
6
  def has_with_trial(ability)
5
7
  ::Avo::App.license.has_with_trial(ability)
6
8
  end
@@ -26,9 +26,11 @@ class Avo::ButtonComponent < ViewComponent::Base
26
26
  def args
27
27
  if @args[:loading]
28
28
  @args[:"data-controller"] = "loading-button"
29
+ @args[:"data-loading-button-confirmed-value"] = false
30
+ @args[:"data-action"] = "click->loading-button#attemptSubmit"
29
31
 
30
32
  if @args[:confirm]
31
- @args[:"data-avo-confirm"] = @args.delete(:confirm)
33
+ @args[:"data-loading-button-confirmation-message-value"] = @args.delete(:confirm)
32
34
  end
33
35
  end
34
36
 
@@ -3,6 +3,7 @@
3
3
  class Avo::Fields::EditComponent < ViewComponent::Base
4
4
  include Avo::ResourcesHelper
5
5
 
6
+ attr_reader :field
6
7
  attr_reader :view
7
8
 
8
9
  def initialize(field: nil, resource: nil, index: 0, form: nil, displayed_in_modal: false)
@@ -17,4 +18,8 @@ class Avo::Fields::EditComponent < ViewComponent::Base
17
18
  def classes(extra_classes = "")
18
19
  helpers.input_classes("#{@field.get_html(:classes, view: view, element: :input)} #{extra_classes}", has_error: @field.model_errors.include?(@field.id))
19
20
  end
21
+
22
+ def render?
23
+ !field.computed
24
+ end
20
25
  end
@@ -5,7 +5,7 @@ class Avo::Fields::ShowComponent < ViewComponent::Base
5
5
 
6
6
  attr_reader :view
7
7
 
8
- def initialize(field: nil, resource: nil, index: 0)
8
+ def initialize(field: nil, resource: nil, index: 0, form: nil)
9
9
  @field = field
10
10
  @resource = resource
11
11
  @index = index
@@ -1,8 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  class Avo::Index::Ordering::ButtonComponent < Avo::Index::Ordering::BaseComponent
4
- delegate :view_context, to: ::Avo::App
5
-
6
4
  attr_accessor :resource
7
5
  attr_accessor :reflection
8
6
  attr_accessor :direction
@@ -20,10 +18,10 @@ class Avo::Index::Ordering::ButtonComponent < Avo::Index::Ordering::BaseComponen
20
18
  end
21
19
 
22
20
  def order_path(args)
23
- if reflection.present?
24
- view_context.avo.associations_order_path(reflection_parent_resource.route_key, params[:id], field.id, resource.model.id, **args)
21
+ if reflection.present?
22
+ Avo::App.view_context.avo.associations_order_path(reflection_parent_resource.route_key, params[:id], field.id, resource.model.id, **args)
25
23
  else
26
- view_context.avo.resources_order_path(resource.route_key, resource.model.id, **args)
24
+ Avo::App.view_context.avo.resources_order_path(resource.route_key, resource.model.id, **args)
27
25
  end
28
26
  end
29
27
  end
@@ -1,6 +1,6 @@
1
1
  <div class="w-full ">
2
2
  <table class="w-full px-4 bg-white" data-resource-name='<%= @resource.model_key %>' data-controller='item-select-all'>
3
- <%= render partial: 'avo/partials/table_header', locals: {fields: @resource.get_fields(reflection: @reflection)} %>
3
+ <%= render partial: 'avo/partials/table_header', locals: {fields: @resource.get_fields(reflection: @reflection, only_root: true)} %>
4
4
  <tbody class="divide-y">
5
5
  <% @resources.each_with_index do |resource, index| %>
6
6
  <% cache_if Avo.configuration.cache_resources_on_index_view, resource.cache_hash(@parent_model), expires_in: 1.day do %>
@@ -9,7 +9,7 @@
9
9
  </div>
10
10
  </td>
11
11
  <% end %>
12
- <% @resource.get_fields(reflection: @reflection).each_with_index do |field, index| %>
12
+ <% @resource.get_fields(reflection: @reflection, only_root: true).each_with_index do |field, index| %>
13
13
  <%= render field.component_for_view(:index).new(field: field, resource: @resource, index: index, parent_model: @parent_model) %>
14
14
  <% end %>
15
15
  <td class="text-right whitespace-nowrap px-2">
@@ -0,0 +1,19 @@
1
+ <% if item.is_tool? %>
2
+ <% if item&.partial.present? %>
3
+ <%= render item.partial, tool: item %>
4
+ <% end %>
5
+ <% elsif item.is_panel? %>
6
+ <%= render Avo::PanelComponent.new(title: item.name, description: item.description, index: index, view: view) do |c| %>
7
+ <% c.body do %>
8
+ <div class="divide-y">
9
+ <% item.items.each_with_index do |field, index| %>
10
+ <%= render field.hydrate(resource: @resource, model: @resource.model, user: resource.user, view: view).component_for_view(view).new(field: field, resource: @resource, index: index, form: form) %>
11
+ <% end %>
12
+ </div>
13
+ <% end %>
14
+ <% end %>
15
+ <% elsif item.is_tab_group? %>
16
+ <%= render tab_group_component %>
17
+ <% elsif item.is_field? %>
18
+ <%= render field_component %>
19
+ <% end %>
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::ItemSwitcherComponent < Avo::BaseComponent
4
+ include Turbo::FramesHelper
5
+
6
+ attr_reader :resource
7
+ attr_reader :reflection
8
+ attr_reader :index
9
+ attr_reader :item
10
+ attr_reader :view
11
+
12
+ def initialize(resource: nil, reflection: nil, item: nil, index: nil, view: nil, form: nil)
13
+ @resource = resource
14
+ @reflection = reflection
15
+ @form = form
16
+ @index = index
17
+ @item = item
18
+ @view = view
19
+ end
20
+
21
+ def form
22
+ @form || nil
23
+ end
24
+
25
+ def render?
26
+ # Stops rendering if the field should be hidden in reflections
27
+ if item.is_field?
28
+ return false if in_reflection? && item.hidden_in_reflection?
29
+ end
30
+
31
+ true
32
+ end
33
+
34
+ def in_reflection?
35
+ @reflection.present?
36
+ end
37
+
38
+ def tab_group_component
39
+ Avo::TabGroupComponent.new resource: @resource, group: item.hydrate(view: view), index: index, params: params, form: form, view: view
40
+ end
41
+
42
+ def field_component
43
+ item.component_for_view(@view).new(field: item.hydrate(resource: @resource, view: @view, model: @resource.model), resource: @resource, index: index, form: form)
44
+ end
45
+ end
@@ -1,4 +1,4 @@
1
- <div <%== data_attributes %>>
1
+ <%= content_tag :div, data: data_attributes, class: classes do %>
2
2
  <% if render_header? %>
3
3
  <div class="flex-1 flex flex-col xl:flex-row justify-between mb-4">
4
4
  <div class="overflow-hidden flex flex-col">
@@ -7,18 +7,15 @@
7
7
  <%= helpers.render_breadcrumbs(separator: helpers.svg('chevron-right', class: 'inline-block h-3 stroke-current relative top-[-1px] ml-1' )) if Avo.configuration.display_breadcrumbs %>
8
8
  </div>
9
9
  <% end %>
10
-
11
10
  <div class="text-2xl tracking-normal font-semibold text-gray-800 truncate items-center flex flex-1" data-target="title">
12
- <span><%= @title %></span>
11
+ <span><%= @name %></span>
13
12
  </div>
14
-
15
13
  <% if description.present? %>
16
- <div class="text-sm tracking-normal font-medium text-gray-600" data-target="description">
17
- <%== description %>
18
- </div>
14
+ <div class="text-sm tracking-normal font-medium text-gray-600" data-target="description">
15
+ <%== description %>
16
+ </div>
19
17
  <% end %>
20
18
  </div>
21
-
22
19
  <% if tools.present? %>
23
20
  <div class="flex-1 w-full flex flex-col sm:flex-row xl:justify-end sm:items-end space-y-2 sm:space-y-0 sm:space-x-2 mt-4 xl:mt-0">
24
21
  <%= tools %>
@@ -26,26 +23,28 @@
26
23
  <% end %>
27
24
  </div>
28
25
  <% end %>
29
-
30
- <div class="relative <%= white_panel_classes %> <%= @body_classes %>">
31
- <%= body %>
32
- </div>
33
-
34
- <div class="relative">
35
- <%= bare_content %>
36
- </div>
37
-
38
- <% if footer_tools.present? %>
26
+ <% if body? %>
27
+ <div class="relative <%= white_panel_classes %> <%= @body_classes %>">
28
+ <%= body %>
29
+ </div>
30
+ <% end %>
31
+ <% if bare_content? %>
32
+ <div class="relative">
33
+ <%= bare_content %>
34
+ </div>
35
+ <% end %>
36
+ <% if footer_tools? %>
39
37
  <div class="<%= white_panel_classes %> p-4 flex-1 flex flex-col xl:flex-row justify-between mt-6">
40
38
  <div class="flex-1 w-full flex flex-col sm:flex-row xl:justify-end sm:items-end space-y-2 sm:space-y-0 sm:space-x-2 mt-4 xl:mt-0">
41
39
  <%= footer_tools %>
42
40
  </div>
43
41
  </div>
44
42
  <% end %>
45
-
46
- <div class="flex justify-end w-full">
47
- <div>
48
- <%= footer %>
43
+ <% if footer? %>
44
+ <div class="flex justify-end w-full">
45
+ <div>
46
+ <%= footer %>
47
+ </div>
49
48
  </div>
50
- </div>
51
- </div>
49
+ <% end %>
50
+ <% end %>
@@ -2,6 +2,8 @@
2
2
 
3
3
  class Avo::PanelComponent < ViewComponent::Base
4
4
  attr_reader :title
5
+ attr_reader :name
6
+ attr_reader :classes
5
7
 
6
8
  renders_one :tools
7
9
  renders_one :body
@@ -9,13 +11,17 @@ class Avo::PanelComponent < ViewComponent::Base
9
11
  renders_one :footer_tools
10
12
  renders_one :footer
11
13
 
12
- def initialize(title: nil, description: nil, body_classes: nil, data: {}, display_breadcrumbs: false, index: nil)
14
+ def initialize(title: nil, name: nil, description: nil, body_classes: nil, data: {}, display_breadcrumbs: false, index: nil, classes: nil, view: nil)
15
+ # deprecating title in favor of name
13
16
  @title = title
17
+ @name = name || title
14
18
  @description = description
19
+ @classes = classes
15
20
  @body_classes = body_classes
16
21
  @data = data
17
22
  @display_breadcrumbs = display_breadcrumbs
18
23
  @index = index
24
+ @view = view
19
25
  end
20
26
 
21
27
  private
@@ -26,9 +32,6 @@ class Avo::PanelComponent < ViewComponent::Base
26
32
 
27
33
  def data_attributes
28
34
  @data.merge({"panel-index": @index})
29
- .map do |key, value|
30
- " data-#{key}=\"#{value}\""
31
- end.join
32
35
  end
33
36
 
34
37
  def display_breadcrumbs?
@@ -42,7 +45,7 @@ class Avo::PanelComponent < ViewComponent::Base
42
45
  end
43
46
 
44
47
  def render_header?
45
- @title.present? || description.present? || tools.present? || display_breadcrumbs?
48
+ @name.present? || description.present? || tools.present? || display_breadcrumbs?
46
49
  end
47
50
 
48
51
  def render_footer_tools?
@@ -0,0 +1,53 @@
1
+ <%= content_tag :div,
2
+ data: {
3
+ target: "tab-group",
4
+ index: index,
5
+ controller: "tabs",
6
+ tabs_view_value: view,
7
+ tabs_active_tab_value: active_tab_name
8
+ },
9
+ class: 'space-y-12' do %>
10
+ <% visible_tabs.each_with_index do |tab, index| %>
11
+ <%
12
+ args = {
13
+ # Hide the turbo frames that aren't in the current tab
14
+ # This way we can lazy load the un-selected tabs on the show view
15
+ class: "block #{'hidden' unless tab.name == active_tab_name}",
16
+ data: {
17
+ # Add a marker to know if we already loaded a turbo frame
18
+ loaded: tab.name == active_tab_name,
19
+ tabs_target: :tab,
20
+ tab_id: tab.name,
21
+ }
22
+ }
23
+
24
+ is_current_tab = active_tab_name.to_s == tab.name.to_s
25
+
26
+ # On edit screens we want to load each tab because we wnst the DOM to have the fields present on form submission.
27
+ # 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.
28
+ # We also want to load the current tab
29
+ should_lazy_load = if @view.to_s.in?(['edit', 'new'])
30
+ false
31
+ else
32
+ !is_current_tab
33
+ end
34
+
35
+ if should_lazy_load
36
+ args[:src] = helpers.resource_path(resource: @resource, model: @resource.model, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id)
37
+ args[:loading] = :lazy
38
+ end
39
+ %>
40
+ <%= turbo_frame_tag tab.turbo_frame_id(parent: @group), **args do %>
41
+ <div class="border rounded-lg p-2 -mx-2 -my-2 lg:p-4 lg:-mx-4 lg:-my-4 space-y-4">
42
+ <%= render Avo::TabSwitcherComponent.new resource: @resource, current_tab: tab, group: group, active_tab_name: active_tab_name, view: view %>
43
+ <% if !should_lazy_load && !tab.empty? %>
44
+ <div class="space-y-12">
45
+ <% tab.visible_items.each do |item| %>
46
+ <%= render Avo::ItemSwitcherComponent.new resource: @resource, item: item, index: index, form: form, view: @view %>
47
+ <% end %>
48
+ </div>
49
+ <% end %>
50
+ </div>
51
+ <% end %>
52
+ <% end %>
53
+ <% end %>
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::TabGroupComponent < Avo::BaseComponent
4
+ attr_reader :group
5
+ attr_reader :index
6
+ attr_reader :view
7
+ attr_reader :form
8
+
9
+ def initialize(resource:, group:, index:, form:, params:, view:)
10
+ @resource = resource
11
+ @group = group
12
+ @index = index
13
+ @form = form
14
+ @params = params
15
+ @view = view
16
+
17
+ @group.index = index
18
+ end
19
+
20
+ def render?
21
+ tabs_have_content? && visible_tabs.present?
22
+ end
23
+
24
+ def tabs_have_content?
25
+ visible_tabs.present?
26
+ end
27
+
28
+ def active_tab_name
29
+ params[:active_tab_name] || group.visible_items&.first&.name
30
+ end
31
+
32
+ def tabs
33
+ @group.items.map do |tab|
34
+ tab.hydrate(view: view)
35
+ end
36
+ end
37
+
38
+ def visible_tabs
39
+ tabs.select do |tab|
40
+ !tab.empty?
41
+ end
42
+ end
43
+
44
+ def active_tab
45
+ return if group.visible_items.blank?
46
+
47
+ group.visible_items.find do |tab|
48
+ tab.name.to_s == active_tab_name.to_s
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ <div class="flex" data-target="tab-switcher">
2
+ <div class="button-group">
3
+ <% visible_items.each do |tab| %>
4
+ <%= a_link tab_path(tab),
5
+ color: selected?(tab) ? :gray : :primary,
6
+ rounded: false,
7
+ size: :sm,
8
+ class: selected?(tab) ? ' bg-gray-100 border-gray-300' : ' z-20',
9
+ title: tab.description,
10
+ data: {
11
+ tippy: tab.description.present? ? 'tooltip' : '',
12
+ control: "view-type-toggle-#{tab.name}",
13
+ selected: selected?(tab),
14
+ action: 'click->tabs#changeTab',
15
+ tabs_id_param: tab.name
16
+ } do %>
17
+ <%= tab.name %>
18
+ <% end %>
19
+ <% end %>
20
+ </div>
21
+ </div>
@@ -0,0 +1,86 @@
1
+ # frozen_string_literal: true
2
+
3
+ class Avo::TabSwitcherComponent < Avo::BaseComponent
4
+ include Avo::UrlHelpers
5
+ include Avo::ApplicationHelper
6
+
7
+ attr_reader :active_tab_name
8
+ attr_reader :group
9
+ attr_reader :current_tab
10
+ attr_reader :tabs
11
+ attr_reader :view
12
+
13
+ def initialize(resource:, group:, current_tab:, active_tab_name:, view:)
14
+ @active_tab_name = active_tab_name
15
+ @resource = resource
16
+ @group = group
17
+ @current_tab = current_tab
18
+ @tabs = group.items
19
+ @view = view
20
+ end
21
+
22
+ def tab_path(tab)
23
+ if is_edit?
24
+ helpers.edit_resource_path(resource: @resource, model: @resource.model, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id)
25
+ elsif is_new?
26
+ helpers.new_resource_path(resource: @resource, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id)
27
+ else
28
+ helpers.resource_path(resource: @resource, model: @resource.model, keep_query_params: true, active_tab_name: tab.name, tab_turbo_frame: group.turbo_frame_id)
29
+ end
30
+ end
31
+
32
+ def is_edit?
33
+ @view == :edit
34
+ end
35
+
36
+ def is_new?
37
+ @view == :new
38
+ end
39
+
40
+ def is_initial_load?
41
+ params[:active_tab_name].blank?
42
+ end
43
+
44
+ # On initial load we want that each tab button to be the selected one.
45
+ # We do that so we don't get the wrongly selected item for a quick brief when first switching from one panel to another.
46
+ def selected?(tab)
47
+ if is_initial_load?
48
+ current_tab.name.to_s == tab.name.to_s
49
+ else
50
+ tab.name.to_s == active_tab_name.to_s
51
+ end
52
+ end
53
+
54
+ # Goes through all items and removes the ones that are not supposed to be visible.
55
+ # Example below:
56
+ # tabs do
57
+ # field :comments, as: :has_many
58
+ # end
59
+ # Because the developer hasn't specified that it should be visible on edit views (with the show_on: :edit option),
60
+ # the field should not be visible in the item switcher either.
61
+ def visible_items
62
+ tabs.select do |item|
63
+ visible = true
64
+
65
+ if item.items.blank?
66
+ visible = false
67
+ end
68
+
69
+ first_item = item.items.first
70
+ if item.items.count == 1 && first_item.is_field? && first_item.has_own_panel? && !first_item.visible_on?(view)
71
+ # Return nil if tab contians a has_many type of fields and it's hidden in current view
72
+ visible = false
73
+ end
74
+
75
+ if item.respond_to?(:visible_on?)
76
+ visible = item.visible_on? view
77
+ end
78
+
79
+ if item.respond_to?(:visible?)
80
+ visible = item.visible?
81
+ end
82
+
83
+ visible
84
+ end
85
+ end
86
+ end