avo 2.9.1.pre5 → 2.10.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.

Files changed (97) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -5
  3. data/README.md +4 -0
  4. data/app/assets/stylesheets/css/buttons.css +4 -1
  5. data/app/components/avo/actions_component.rb +6 -2
  6. data/app/components/avo/base_component.rb +2 -0
  7. data/app/components/avo/button_component.rb +3 -1
  8. data/app/components/avo/fields/common/key_value_component.html.erb +2 -2
  9. data/app/components/avo/fields/common/single_file_viewer_component.rb +1 -1
  10. data/app/components/avo/fields/edit_component.rb +5 -0
  11. data/app/components/avo/fields/show_component.rb +1 -1
  12. data/app/components/avo/index/ordering/button_component.rb +2 -12
  13. data/app/components/avo/index/resource_controls_component.html.erb +2 -2
  14. data/app/components/avo/index/resource_controls_component.rb +5 -1
  15. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  16. data/app/components/avo/index/table_row_component.html.erb +1 -1
  17. data/app/components/avo/item_switcher_component.html.erb +19 -0
  18. data/app/components/avo/item_switcher_component.rb +45 -0
  19. data/app/components/avo/panel_component.html.erb +23 -24
  20. data/app/components/avo/panel_component.rb +8 -5
  21. data/app/components/avo/tab_group_component.html.erb +53 -0
  22. data/app/components/avo/tab_group_component.rb +51 -0
  23. data/app/components/avo/tab_switcher_component.html.erb +21 -0
  24. data/app/components/avo/tab_switcher_component.rb +86 -0
  25. data/app/components/avo/views/resource_edit_component.html.erb +34 -56
  26. data/app/components/avo/views/resource_edit_component.rb +10 -0
  27. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  28. data/app/components/avo/views/resource_index_component.rb +3 -3
  29. data/app/components/avo/views/resource_show_component.html.erb +58 -89
  30. data/app/components/avo/views/resource_show_component.rb +2 -2
  31. data/app/controllers/avo/actions_controller.rb +1 -1
  32. data/app/controllers/avo/application_controller.rb +20 -3
  33. data/app/helpers/avo/application_helper.rb +0 -6
  34. data/app/helpers/avo/url_helpers.rb +1 -1
  35. data/app/javascript/avo.js +5 -1
  36. data/app/javascript/js/controllers/loading_button_controller.js +25 -21
  37. data/app/javascript/js/controllers/tabs_controller.js +86 -0
  38. data/app/javascript/js/controllers.js +2 -0
  39. data/app/views/avo/base/index.html.erb +1 -1
  40. data/app/views/avo/base/show.html.erb +1 -1
  41. data/app/views/avo/cards/show.html.erb +1 -1
  42. data/app/views/avo/debug/index.html.erb +1 -1
  43. data/app/views/avo/home/_actions.html.erb +1 -1
  44. data/app/views/avo/home/_dashboards.html.erb +19 -0
  45. data/app/views/avo/home/_filters.html.erb +1 -1
  46. data/app/views/avo/home/_resources.html.erb +1 -1
  47. data/app/views/avo/home/failed_to_load.html.erb +1 -1
  48. data/app/views/avo/home/index.html.erb +14 -2
  49. data/app/views/avo/partials/_javascript.html.erb +1 -1
  50. data/app/views/avo/partials/_tabs_toggle.html.erb +20 -0
  51. data/app/views/avo/private/design.html.erb +1 -1
  52. data/config/routes.rb +1 -1
  53. data/lib/avo/app.rb +9 -2
  54. data/lib/avo/base_action.rb +2 -19
  55. data/lib/avo/base_card.rb +1 -7
  56. data/lib/avo/base_resource.rb +0 -94
  57. data/lib/avo/base_resource_tool.rb +3 -1
  58. data/lib/avo/concerns/has_fields.rb +247 -50
  59. data/lib/avo/concerns/has_html_attributes.rb +1 -1
  60. data/lib/avo/concerns/is_resource_item.rb +36 -0
  61. data/lib/avo/dashboards/base_dashboard.rb +1 -1
  62. data/lib/avo/dsl/field_parser.rb +83 -0
  63. data/lib/avo/fields/base_field.rb +19 -2
  64. data/lib/avo/fields/field_extensions/visible_in_different_views.rb +18 -1
  65. data/lib/avo/fields/has_base_field.rb +20 -1
  66. data/lib/avo/fields/has_one_field.rb +4 -1
  67. data/lib/avo/grid_collector.rb +6 -3
  68. data/lib/avo/items_holder.rb +68 -0
  69. data/lib/avo/licensing/h_q.rb +10 -0
  70. data/lib/avo/main_panel.rb +3 -0
  71. data/lib/avo/menu/builder.rb +7 -7
  72. data/lib/avo/panel.rb +25 -0
  73. data/lib/avo/panel_builder.rb +23 -0
  74. data/lib/avo/services/uri_service.rb +71 -0
  75. data/lib/avo/tab.rb +78 -0
  76. data/lib/avo/tab_builder.rb +25 -0
  77. data/lib/avo/tab_group.rb +40 -0
  78. data/lib/avo/tab_group_builder.rb +43 -0
  79. data/lib/avo/version.rb +1 -1
  80. data/lib/avo.rb +1 -0
  81. data/lib/generators/avo/templates/resource/controller.tt +2 -0
  82. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  83. data/lib/generators/avo/templates/tool/view.tt +1 -1
  84. data/public/avo-assets/avo.css +27 -3
  85. data/public/avo-assets/avo.js +73 -73
  86. data/public/avo-assets/avo.js.map +3 -3
  87. metadata +24 -14
  88. data/app/assets/builds/action_cable.js +0 -2
  89. data/app/assets/builds/action_cable.js.map +0 -7
  90. data/app/assets/builds/application.js +0 -2
  91. data/app/assets/builds/application.js.map +0 -7
  92. data/app/assets/builds/avo.css +0 -9028
  93. data/app/assets/builds/avo.js +0 -512
  94. data/app/assets/builds/avo.js.map +0 -7
  95. data/app/assets/builds/avo_custom.js +0 -6
  96. data/app/assets/builds/avo_custom.js.map +0 -7
  97. data/lib/avo/concerns/has_tools.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 6512a0f8b1ec04fdbc67d8929d47113b9711d38b014e7784b6c25bccaf0163de
4
- data.tar.gz: 95e7535efc1dec6f0ce785953cc6324de2776f56465d2a3f1cd824022f436c91
3
+ metadata.gz: 75872d452d6a4918c39857b6dbc0075477f13a87fa42d6bba3cc1da5fa924be0
4
+ data.tar.gz: e3b24ecd70d7fc005a33fd4b4fd34b79fd1c228db6125be80574968f639677b8
5
5
  SHA512:
6
- metadata.gz: 7ee889a86a4db15637aa7e3c67f6f3d39df08d4642aa8710bbe622eb3787621a3d071da9144f2bc498fbda4443e9a99afdf60842eafa8c8f279399306cb956c9
7
- data.tar.gz: 5cbc078905436417b6051b6afe3607751cc1ebe7373588bb6656b88bbb0ff0c446fdebd4c97b72c468aff03920aba6f040e27a3ee622f6a4c8841926922cda10
6
+ metadata.gz: 88ba446afaeca394f7231314e12e87d535c995fc92009f1f9010729a43a817680fd2290e8513bc643a5b4c446c3f4323b4db15f3f073b536b138ca8751a8d2dc
7
+ data.tar.gz: f3e4ca2474e497d4693c68cb899d724ce511bba93a55db8d2c9666658c3f46c4744d635ed489001e9a9ca4e3921979fc503329a64b8686e875e2072a3033ce84
data/Gemfile.lock CHANGED
@@ -1,7 +1,7 @@
1
1
  PATH
2
2
  remote: .
3
3
  specs:
4
- avo (2.9.1.pre5)
4
+ avo (2.10.0)
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)
@@ -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)
@@ -391,7 +391,7 @@ GEM
391
391
  tzinfo (2.0.4)
392
392
  concurrent-ruby (~> 1.0)
393
393
  unicode-display_width (2.1.0)
394
- view_component (2.49.1)
394
+ view_component (2.57.1)
395
395
  activesupport (>= 5.0.0, < 8.0)
396
396
  method_source (~> 1.0)
397
397
  warden (1.2.9)
@@ -414,7 +414,7 @@ GEM
414
414
  websocket-extensions (0.1.5)
415
415
  xpath (3.2.0)
416
416
  nokogiri (~> 1.8)
417
- zeitwerk (2.5.4)
417
+ zeitwerk (2.6.0)
418
418
 
419
419
  PLATFORMS
420
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 {
@@ -43,10 +43,14 @@ class Avo::ActionsComponent < ViewComponent::Base
43
43
  end
44
44
 
45
45
  def single_record_path(id)
46
- "#{@resource.record_path}/actions/#{id}"
46
+ Avo::Services::URIService.parse(@resource.record_path)
47
+ .append_paths("actions", id)
48
+ .to_s
47
49
  end
48
50
 
49
51
  def many_records_path(id)
50
- "#{@resource.records_path}/actions/#{id}"
52
+ Avo::Services::URIService.parse(@resource.records_path)
53
+ .append_paths("actions", id)
54
+ .to_s
51
55
  end
52
56
  end
@@ -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,7 +3,7 @@
3
3
  data-key-value-target="controller"
4
4
  data-options="<%= @field.options.to_json %>"
5
5
  data-input-classes="<%= input_classes %>"
6
- data-editable="<%= @view.in?([:edit, :create]) %>"
6
+ data-editable="<%= @view.in?([:edit, :new]) %>"
7
7
  >
8
8
  <div class="w-full flex flex-col">
9
9
  <div class="flex w-full">
@@ -14,7 +14,7 @@
14
14
  <div class="w-1/2 py-3 px-3 uppercase font-semibold text-xs text-white">
15
15
  <%= @field.value_label %>
16
16
  </div>
17
- <% if @view.in?([:edit, :create]) %>
17
+ <% if @view.in?([:edit, :new]) %>
18
18
  <div class="flex items-center justify-center p-2 px-3 border-l border-gray-600">
19
19
  <a href="javascript:void(0);"
20
20
  title="<%= @field.action_text %>"
@@ -10,7 +10,7 @@ class Avo::Fields::Common::SingleFileViewerComponent < ViewComponent::Base
10
10
  end
11
11
 
12
12
  def destroy_path
13
- "#{@resource.record_path}/active_storage_attachments/#{id}/#{file.id}"
13
+ Avo::Services::URIService.parse(@resource.record_path).append_paths("active_storage_attachments", id, file.id).to_s
14
14
  end
15
15
 
16
16
  def id
@@ -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
@@ -19,19 +19,9 @@ class Avo::Index::Ordering::ButtonComponent < Avo::Index::Ordering::BaseComponen
19
19
 
20
20
  def order_path(args)
21
21
  if reflection.present?
22
- path = "#{::Avo::App.root_path}/resources/#{reflection_parent_resource.route_key}/#{params[:id]}/#{field.id}/#{resource.model.id}/order"
22
+ Avo::App.view_context.avo.associations_order_path(reflection_parent_resource.route_key, params[:id], field.id, resource.model.id, **args)
23
23
  else
24
- path = "#{::Avo::App.root_path}/resources/#{resource.route_key}/#{resource.model.id}/order"
24
+ Avo::App.view_context.avo.resources_order_path(resource.route_key, resource.model.id, **args)
25
25
  end
26
-
27
- if args.present?
28
- string_args = args.map do |key, value|
29
- "#{key}=#{value}"
30
- end.join('&')
31
-
32
- path = "#{path}?#{string_args}"
33
- end
34
-
35
- path
36
26
  end
37
27
  end
@@ -46,7 +46,7 @@
46
46
  }
47
47
  %>
48
48
  <%= hidden_field_tag :turbo_frame, params[:turbo_frame], id: "turbo_frame_detach_#{@resource.model.id}" if params[:turbo_frame] %>
49
- <%= hidden_field_tag :referrer, request.fullpath, id: "referrer_detach_#{@resource.model.id}" if params[:turbo_frame] %>
49
+ <%= hidden_field_tag :referrer, referrer_path, id: "referrer_detach_#{@resource.model.id}" if params[:turbo_frame] %>
50
50
  <% end %>
51
51
  <% end %>
52
52
 
@@ -71,7 +71,7 @@
71
71
  %>
72
72
  <%= form.hidden_field :view_type, value: params[:view_type], id: "turbo_view_type_#{@resource.model.id}" if params[:view_type] %>
73
73
  <%= form.hidden_field :turbo_frame, value: params[:turbo_frame], id: "turbo_frame_destroy_#{@resource.model.id}" if params[:turbo_frame] %>
74
- <%= form.hidden_field :referrer, value: request.fullpath, id: "referrer_destroy_#{@resource.model.id}" if params[:turbo_frame] %>
74
+ <%= form.hidden_field :referrer, value: referrer_path, id: "referrer_destroy_#{@resource.model.id}" if params[:turbo_frame] %>
75
75
  <% end %>
76
76
  <% end %>
77
77
  </div>
@@ -41,7 +41,7 @@ class Avo::Index::ResourceControlsComponent < Avo::ResourceComponent
41
41
  end
42
42
 
43
43
  def edit_path
44
- #Add the `view` param to let Avo know where to redirect back when the user clicks the `Cancel` button.
44
+ # Add the `view` param to let Avo know where to redirect back when the user clicks the `Cancel` button.
45
45
  args = {via_view: 'index'}
46
46
 
47
47
  if @parent_model.present?
@@ -71,4 +71,8 @@ class Avo::Index::ResourceControlsComponent < Avo::ResourceComponent
71
71
  def is_has_many_association
72
72
  @reflection.is_a?(::ActiveRecord::Reflection::HasManyReflection) || @reflection.is_a?(::ActiveRecord::Reflection::ThroughReflection)
73
73
  end
74
+
75
+ def referrer_path
76
+ Avo::App.root_path(paths: ['resources', params[:resource_name], params[:id], params[:related_name]], query: request.query_parameters.to_h)
77
+ end
74
78
  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