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
@@ -1,77 +1,55 @@
1
1
  <%= content_tag :div,
2
- class: "space-y-12",
3
2
  data: {
4
3
  'model-id': @resource.model.id,
5
4
  selected_resources_name: @resource.model_key,
6
5
  selected_resources: [@resource.model.id],
7
6
  **@resource.stimulus_data_attributes
8
7
  } do %>
9
- <% @resource.panels.each_with_index do |resource_panel, index| %>
10
- <%= form_with model: @resource.model,
11
- scope: @resource.form_scope,
12
- url: form_url,
13
- local: true,
14
- multipart: true do |form| %>
15
- <%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
16
- <%= render Avo::PanelComponent.new(title: resource_panel[:name], description: @resource.resource_description, display_breadcrumbs: true, index: index) do |c| %>
17
- <% c.tools do %>
18
- <%= a_link back_path,
19
- style: :text,
20
- icon: 'arrow-left' do %>
21
- <%= t('avo.cancel').capitalize %>
22
- <% end %>
23
- <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
24
- <% if can_see_the_save_button? %>
25
- <%= a_button color: :primary,
8
+ <%= form_with model: @resource.model,
9
+ scope: @resource.form_scope,
10
+ url: form_url,
11
+ method: form_method,
12
+ local: true,
13
+ html: {
14
+ novalidate: true
15
+ },
16
+ multipart: true do |form| %>
17
+ <%= render Avo::ReferrerParamsComponent.new back_path: back_path %>
18
+ <%= render Avo::PanelComponent.new(title: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
19
+ <% c.tools do %>
20
+ <%= a_link back_path,
21
+ style: :text,
22
+ icon: 'arrow-left' do %>
23
+ <%= t('avo.cancel').capitalize %>
24
+ <% end %>
25
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
26
+ <% if can_see_the_save_button? %>
27
+ <%= a_button color: :primary,
26
28
  style: :primary,
27
29
  loading: true,
28
30
  type: :submit,
29
31
  icon: 'save' do %>
30
- <%= t('avo.save').capitalize %>
31
- <% end %>
32
+ <%= t('avo.save').capitalize %>
32
33
  <% end %>
33
34
  <% end %>
34
- <% if Avo.configuration.buttons_on_form_footers %>
35
- <% c.footer_tools do %>
36
- <%= a_link back_path, icon: 'arrow-left' do %>
37
- <%= t('avo.cancel').capitalize %>
38
- <% end %>
39
- <% if can_see_the_save_button? %>
40
- <%= a_button color: :green, loading: true, type: :submit, icon: 'save' do %>
41
- <%= t('avo.save').capitalize %>
42
- <% end %>
43
- <% end %>
35
+ <% end %>
36
+ <% if Avo.configuration.buttons_on_form_footers %>
37
+ <% c.footer_tools do %>
38
+ <%= a_link back_path, icon: 'arrow-left' do %>
39
+ <%= t('avo.cancel').capitalize %>
44
40
  <% end %>
45
- <% end %>
46
- <% c.body do %>
47
- <div class="divide-y">
48
- <% @resource.get_fields.each_with_index do |field, index| %>
49
- <%= render field.component_for_view(:edit).new(field: field, resource: @resource, index: index, form: form) unless field.computed %>
41
+ <% if can_see_the_save_button? %>
42
+ <%= a_button color: :green, loading: true, type: :submit, icon: 'save' do %>
43
+ <%= t('avo.save').capitalize %>
50
44
  <% end %>
51
- </div>
45
+ <% end %>
52
46
  <% end %>
53
47
  <% end %>
54
48
  <% end %>
55
- <% end %>
56
- <% if @reflection.blank? %>
57
- <% if has_one_panels.present? %>
58
- <% has_one_panels.each_with_index do |field, index| %>
59
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
60
- <% end %>
61
- <% end %>
62
- <% if has_many_panels.present? %>
63
- <% has_many_panels.each_with_index do |field, index| %>
64
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
65
- <% end %>
66
- <% end %>
67
- <% if has_as_belongs_to_many_panels.present? %>
68
- <% has_as_belongs_to_many_panels.each_with_index do |field, index| %>
69
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
70
- <% end %>
71
- <% end %>
72
- <% if resource_tools.present? %>
73
- <% resource_tools.each do |tool, index| %>
74
- <%= render tool.partial, tool: tool %>
49
+ <%= content_tag :div, class: 'space-y-12' do %>
50
+ <% @resource.get_items.each_with_index do |item, index| %>
51
+ <% next if item.nil? %>
52
+ <%= render Avo::ItemSwitcherComponent.new resource: @resource, item: item, index: index + 1, view: @view, form: form %>
75
53
  <% end %>
76
54
  <% end %>
77
55
  <% end %>
@@ -15,6 +15,10 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
15
15
  split_panel_fields
16
16
  end
17
17
 
18
+ def title
19
+ @resource.default_panel_name
20
+ end
21
+
18
22
  def back_path
19
23
  if via_resource?
20
24
  helpers.resource_path(model: params[:via_resource_class].safe_constantize, resource: relation_resource, resource_id: params[:via_resource_id])
@@ -45,6 +49,12 @@ class Avo::Views::ResourceEditComponent < Avo::ResourceComponent
45
49
  view == :edit
46
50
  end
47
51
 
52
+ def form_method
53
+ return :put if is_edit?
54
+
55
+ :post
56
+ end
57
+
48
58
  def form_url
49
59
  if is_edit?
50
60
  helpers.resource_path(
@@ -37,7 +37,7 @@
37
37
  <%= render partial: 'avo/partials/resource_search', locals: {resource: @resource.model_name.collection} %>
38
38
  </div>
39
39
  <% else %>
40
- <%# Offset for the space-y-2 property when the serach is missing %>
40
+ <%# Offset for the space-y-2 property when the search is missing %>
41
41
  <div class="-mb-2"></div>
42
42
  <% end %>
43
43
  <% if @filters.present? || available_view_types.count > 1 %>
@@ -113,11 +113,11 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
113
113
  end
114
114
  end
115
115
 
116
- helpers.new_resource_path(model: @resource.model_class, resource: @resource, **args)
116
+ helpers.new_resource_path(resource: @resource, **args)
117
117
  end
118
118
 
119
119
  def attach_path
120
- "#{Avo::App.root_path}#{request.env["PATH_INFO"]}/new"
120
+ Avo::App.root_path(paths: [request.env["PATH_INFO"], "new"])
121
121
  end
122
122
 
123
123
  def singular_resource_name
@@ -145,7 +145,7 @@ class Avo::Views::ResourceIndexComponent < Avo::ResourceComponent
145
145
  @reflection.active_record.to_s
146
146
  end
147
147
 
148
- def name
148
+ def name
149
149
  field.custom_name? ? field.name : field.plural_name
150
150
  end
151
151
 
@@ -1,105 +1,74 @@
1
1
  <%= content_tag :div,
2
- class: "space-y-12",
3
2
  data: {
4
3
  'model-id': @resource.model.id,
5
4
  selected_resources_name: @resource.model_key,
6
5
  selected_resources: [@resource.model.id],
7
6
  **@resource.stimulus_data_attributes
8
7
  } do %>
9
- <% @resource.panels.each_with_index do |resource_panel, index| %>
10
- <%= render Avo::PanelComponent.new(title: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: index) do |c| %>
11
- <% c.tools do %>
12
- <% if resource_panel[:name] == @resource.default_panel_name %>
13
- <% if @reflection.present? && @resource.model.present? %>
14
- <% if can_detach? %>
15
- <%= a_button url: detach_path,
16
- icon: 'detach',
17
- method: :delete,
18
- form_class: 'flex flex-col sm:flex-row sm:inline-flex',
19
- style: :text,
20
- data: {
21
- confirm: "Are you sure you want to detach this #{title}."
22
- } do %>
23
- <%= t('avo.detach_item', item: title).capitalize %>
24
- <% end %>
25
- <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
26
- <% end %>
27
- <% if can_see_the_edit_button? %>
28
- <%= a_link edit_path,
29
- color: :primary,
30
- style: :primary,
31
- icon: 'edit' do %>
32
- <%= t('avo.edit').capitalize %>
33
- <% end %>
34
- <% end %>
35
- <% else %>
36
- <%= a_link back_path,
37
- style: :text,
38
- icon: 'arrow-left' do %>
39
- <%= t('avo.go_back') %>
40
- <% end %>
41
- <% if can_see_the_destroy_button? %>
42
- <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
43
- method: :delete,
44
- local: true,
45
- style: :text,
46
- title: t('avo.delete_item', item: @resource.model.model_name.name.downcase).capitalize,
47
- loading: true,
48
- confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
49
- color: :red,
50
- icon: 'trash',
51
- form_class: 'flex flex-col sm:flex-row sm:inline-flex',
52
- data: {
53
- control: :destroy,
54
- 'resource-id': @resource.model.id,
55
- 'tippy': 'tooltip',
56
- } do %>
57
- <%= t('avo.delete').capitalize %>
58
- <% end %>
59
- <% end %>
60
- <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
61
- <% if @resource.authorization.authorize_action(:edit, raise_exception: false) %>
62
- <%= a_link edit_path,
63
- color: :primary,
64
- style: :primary,
65
- icon: 'edit' do %>
66
- <%= t('avo.edit').capitalize %>
67
- <% end %>
68
- <% end %>
8
+ <%= render Avo::PanelComponent.new(title: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
9
+ <% c.tools do %>
10
+ <% if @reflection.present? && @resource.model.present? %>
11
+ <% if can_detach? %>
12
+ <%= a_button url: detach_path,
13
+ icon: 'detach',
14
+ method: :delete,
15
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
16
+ style: :text,
17
+ data: {
18
+ confirm: "Are you sure you want to detach this #{title}."
19
+ } do %>
20
+ <%= t('avo.detach_item', item: title).capitalize %>
69
21
  <% end %>
22
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
70
23
  <% end %>
71
- <% end %>
72
- <% c.body do %>
73
- <% if fields_by_panel[resource_panel[:name]].present? %>
74
- <div class="divide-y">
75
- <% fields_by_panel[resource_panel[:name]].each_with_index do |field, index| %>
76
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
77
- <% end %>
78
- </div>
24
+ <% if can_see_the_edit_button? %>
25
+ <%= a_link edit_path,
26
+ color: :primary,
27
+ style: :primary,
28
+ icon: 'edit' do %>
29
+ <%= t('avo.edit').capitalize %>
30
+ <% end %>
31
+ <% end %>
32
+ <% else %>
33
+ <%= a_link back_path,
34
+ style: :text,
35
+ icon: 'arrow-left' do %>
36
+ <%= t('avo.go_back') %>
37
+ <% end %>
38
+ <% if can_see_the_destroy_button? %>
39
+ <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
40
+ method: :delete,
41
+ local: true,
42
+ style: :text,
43
+ loading: true,
44
+ confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
45
+ color: :red,
46
+ icon: 'trash',
47
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
48
+ data: {
49
+ control: :destroy,
50
+ 'resource-id': @resource.model.id,
51
+ 'tippy': 'tooltip',
52
+ } do %>
53
+ <%= t('avo.delete').capitalize %>
54
+ <% end %>
55
+ <% end %>
56
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
57
+ <% if @resource.authorization.authorize_action(:edit, raise_exception: false) %>
58
+ <%= a_link edit_path,
59
+ color: :primary,
60
+ style: :primary,
61
+ icon: 'edit' do %>
62
+ <%= t('avo.edit').capitalize %>
63
+ <% end %>
79
64
  <% end %>
80
65
  <% end %>
81
66
  <% end %>
82
67
  <% end %>
83
- <% if @reflection.blank? %>
84
- <% if has_one_panels.present? %>
85
- <% has_one_panels.each_with_index do |field, index| %>
86
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
87
- <% end %>
88
- <% end %>
89
- <% if has_many_panels.present? %>
90
- <% has_many_panels.each_with_index do |field, index| %>
91
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
92
- <% end %>
93
- <% end %>
94
- <% if has_as_belongs_to_many_panels.present? %>
95
- <% has_as_belongs_to_many_panels.each_with_index do |field, index| %>
96
- <%= render field.component_for_view(:show).new(field: field, resource: @resource, index: index) %>
97
- <% end %>
98
- <% end %>
99
- <% if resource_tools.present? %>
100
- <% resource_tools.each do |tool, index| %>
101
- <%= render tool.partial, tool: tool %>
102
- <% end %>
68
+ <%= content_tag :div, class: 'space-y-12' do %>
69
+ <% @resource.get_items.each_with_index do |item, index| %>
70
+ <% next if item.nil? %>
71
+ <%= render Avo::ItemSwitcherComponent.new resource: @resource, reflection: @reflection, item: item, index: index + 1, view: @view %>
103
72
  <% end %>
104
73
  <% end %>
105
74
  <% if should_display_invalid_fields_errors? %>
@@ -19,7 +19,7 @@ class Avo::Views::ResourceShowComponent < Avo::ResourceComponent
19
19
  return field.name if has_one_field?
20
20
  reflection_resource.name
21
21
  else
22
- @resource.panels.first[:name]
22
+ @resource.default_panel_name
23
23
  end
24
24
  end
25
25
 
@@ -52,6 +52,6 @@ class Avo::Views::ResourceShowComponent < Avo::ResourceComponent
52
52
  end
53
53
 
54
54
  def has_one_field?
55
- field.present? and field.class == Avo::Fields::HasOneField
55
+ field.present? and field.instance_of? Avo::Fields::HasOneField
56
56
  end
57
57
  end
@@ -42,7 +42,7 @@ module Avo
42
42
  model = @resource.class.find_scope.find params[:id]
43
43
  end
44
44
 
45
- @action = action_class.new(model: model, resource: resource, user: _current_user)
45
+ @action = action_class.new(model: model, resource: resource, user: _current_user, view: :edit)
46
46
  end
47
47
 
48
48
  def respond(response)
@@ -13,7 +13,8 @@ module Avo
13
13
  protect_from_forgery with: :exception
14
14
  before_action :init_app
15
15
  before_action :check_avo_license
16
- before_action :set_locale
16
+ before_action :set_default_locale
17
+ around_action :set_force_locale, if: -> { params[:force_locale].present? }
17
18
  before_action :set_authorization
18
19
  before_action :_authenticate!
19
20
  before_action :set_container_classes
@@ -27,7 +28,7 @@ module Avo
27
28
  add_flash_types :info, :warning, :success, :error
28
29
 
29
30
  def init_app
30
- Avo::App.init request: request, context: context, root_path: avo.root_path.delete_suffix("/"), current_user: _current_user, view_context: view_context, params: params
31
+ Avo::App.init request: request, context: context, current_user: _current_user, view_context: view_context, params: params
31
32
 
32
33
  @license = Avo::App.license
33
34
  end
@@ -287,10 +288,26 @@ module Avo
287
288
  @resource.form_scope
288
289
  end
289
290
 
290
- def set_locale
291
+ def set_default_locale
291
292
  I18n.locale = params[:set_locale] || I18n.default_locale
292
293
 
293
294
  I18n.default_locale = I18n.locale
294
295
  end
296
+
297
+ # Temporary set the locale and reverting at the end of the request.
298
+ def set_force_locale
299
+ initial_locale = I18n.locale.to_s.dup
300
+ I18n.locale = params[:force_locale]
301
+ yield
302
+ I18n.locale = initial_locale
303
+ end
304
+
305
+ def default_url_options
306
+ if params[:force_locale].present?
307
+ { **super, force_locale: params[:force_locale] }
308
+ else
309
+ super
310
+ end
311
+ end
295
312
  end
296
313
  end
@@ -20,12 +20,6 @@ module Avo
20
20
  render Avo::EmptyStateComponent.new **args
21
21
  end
22
22
 
23
- def turbo_frame_wrap(name, &block)
24
- render Avo::TurboFrameWrapperComponent.new name do
25
- capture(&block)
26
- end
27
- end
28
-
29
23
  def a_button(**args, &block)
30
24
  render Avo::ButtonComponent.new(is_link: false, **args) do
31
25
  capture(&block) if block_given?
@@ -35,7 +35,7 @@ module Avo
35
35
  avo.send :"resources_#{resource.singular_route_key}_path", id, **args
36
36
  end
37
37
 
38
- def new_resource_path(model:, resource:, **args)
38
+ def new_resource_path(resource:, **args)
39
39
  avo.send :"new_resources_#{resource.singular_route_key}_path", **args
40
40
  end
41
41
 
@@ -74,7 +74,11 @@ document.addEventListener('turbo:frame-load', () => {
74
74
  document.addEventListener('turbo:before-fetch-response', async (e) => {
75
75
  if (e.detail.fetchResponse.response.status === 500) {
76
76
  const { id, src } = e.target
77
- e.target.src = `${window.Avo.configuration.root_path}/failed_to_load?turbo_frame=${id}&src=${src}`
77
+ // Don't try to redirect to failed to load if this is alread a redirection to failed to load and crashed somewhere.
78
+ // You'll end up with a request loop.
79
+ if (!e.detail.fetchResponse?.response?.url?.includes('/failed_to_load')) {
80
+ e.target.src = `${window.Avo.configuration.root_path}/failed_to_load?turbo_frame=${id}&src=${src}`
81
+ }
78
82
  }
79
83
  })
80
84
 
@@ -7,33 +7,37 @@ export default class extends Controller {
7
7
  <div class="double-bounce2"></div>
8
8
  </div>`;
9
9
 
10
- confirmed = false
11
-
12
- connect() {
13
- this.context.scope.element.addEventListener('click', (e) => {
14
- // If the user has to confirm the action
15
- if (this.confirmationMessage) {
16
- // Intervene only if not confirmed
17
- if (!this.confirmed) {
18
- e.preventDefault()
19
- if (window.confirm(this.confirmationMessage)) {
20
- this.applyLoader()
21
- }
22
- }
23
- } else {
10
+ static values = {
11
+ confirmationMessage: String,
12
+ confirmed: Boolean,
13
+ }
14
+
15
+ attemptSubmit(e) {
16
+ // If the user has to confirm the action
17
+ if (this.confirmationMessageValue) {
18
+ this.confirmAndApply(e)
19
+ } else {
20
+ this.applyLoader()
21
+ }
22
+
23
+ return null
24
+ }
25
+
26
+ confirmAndApply(e) {
27
+ // Intervene only if not confirmed
28
+ if (!this.confirmedValue) {
29
+ e.preventDefault()
30
+
31
+ if (window.confirm(this.confirmationMessageValue)) {
24
32
  this.applyLoader()
25
33
  }
26
- })
34
+ }
27
35
  }
28
36
 
29
37
  get button() {
30
38
  return this.context.scope.element
31
39
  }
32
40
 
33
- get confirmationMessage() {
34
- return this.context.scope.element.getAttribute('data-avo-confirm')
35
- }
36
-
37
41
  applyLoader() {
38
42
  const { button } = this
39
43
 
@@ -50,10 +54,10 @@ export default class extends Controller {
50
54
  }
51
55
 
52
56
  markConfirmed() {
53
- this.confirmed = true
57
+ this.confirmedValue = true
54
58
  }
55
59
 
56
60
  markUnconfirmed() {
57
- this.confirmed = false
61
+ this.confirmedValue = false
58
62
  }
59
63
  }
@@ -0,0 +1,86 @@
1
+ import { AttributeObserver } from '@stimulus/mutation-observers'
2
+ import { Controller } from '@hotwired/stimulus'
3
+ import { castBoolean } from '../helpers/cast_boolean'
4
+
5
+ export default class extends Controller {
6
+ static targets = ['tab'];
7
+
8
+ static values = {
9
+ view: String,
10
+ activeTab: String,
11
+ };
12
+
13
+ get currentTab() {
14
+ return this.tabTargets.find(
15
+ (element) => element.dataset.tabId === this.activeTabValue,
16
+ )
17
+ }
18
+
19
+ targetTab(id) {
20
+ return this.tabTargets.find((element) => element.dataset.tabId === id)
21
+ }
22
+
23
+ changeTab(e) {
24
+ e.preventDefault()
25
+
26
+ const { params } = e
27
+ const { id } = params
28
+
29
+ this.setTheTargetPanelHeight(id)
30
+
31
+ this.hideTabs()
32
+ this.showTab(id)
33
+ this.markTabLoaded(id)
34
+
35
+ this.activeTabValue = id
36
+ }
37
+
38
+ /**
39
+ * Sets the target container height to the previous panel height so we don't get jerky tab changes.
40
+ */
41
+ setTheTargetPanelHeight(id) {
42
+ // Ignore this on edit.
43
+ // All tabs are loaded beforehand, they have their own height, and the page won't jiggle when the user toggles between them.
44
+ if (this.viewValue === 'edit' || this.viewValue === 'new') {
45
+ return
46
+ }
47
+
48
+ // We don't need to add a height to this panel because it was loaded before
49
+ if (castBoolean(this.targetTab(id).dataset.loaded)) {
50
+ return
51
+ }
52
+
53
+ // Get the height of the active panel
54
+ const { height } = this.currentTab.getBoundingClientRect()
55
+ // Set it to the target panel
56
+ this.targetTab(id).style.height = `${height}px`
57
+
58
+ // Wait until the panel loaded it's content and then remove the forced height
59
+ const observer = new AttributeObserver(this.targetTab(id), 'busy', {
60
+ elementUnmatchedAttribute: () => {
61
+ // The content is not available in an instant so delay the height reset a bit.
62
+ setTimeout(() => {
63
+ this.targetTab(id).style.height = ''
64
+ }, 300)
65
+ if (observer) observer.stop()
66
+ },
67
+ })
68
+ observer.start()
69
+ }
70
+
71
+ markTabLoaded(id) {
72
+ this.targetTab(id).dataset.loaded = true
73
+ }
74
+
75
+ showTab(id) {
76
+ this.tabTargets.forEach((element) => {
77
+ if (element.dataset.tabId === id) {
78
+ element.classList.remove('hidden')
79
+ }
80
+ })
81
+ }
82
+
83
+ hideTabs() {
84
+ this.tabTargets.map((element) => element.classList.add('hidden'))
85
+ }
86
+ }
@@ -27,6 +27,7 @@ import SearchController from './controllers/search_controller'
27
27
  import SelectController from './controllers/select_controller'
28
28
  import SelectFilterController from './controllers/select_filter_controller'
29
29
  import SimpleMdeController from './controllers/fields/simple_mde_controller'
30
+ import TabsController from './controllers/tabs_controller'
30
31
  import TagsFieldController from './controllers/fields/tags_field_controller'
31
32
  import TextFilterController from './controllers/text_filter_controller'
32
33
  import TippyController from './controllers/tippy_controller'
@@ -55,6 +56,7 @@ application.register('resource-show', ResourceShowController)
55
56
  application.register('search', SearchController)
56
57
  application.register('select', SelectController)
57
58
  application.register('select-filter', SelectFilterController)
59
+ application.register('tabs', TabsController)
58
60
  application.register('tags-field', TagsFieldController)
59
61
  application.register('text-filter', TextFilterController)
60
62
  application.register('tippy', TippyController)
@@ -1,4 +1,4 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceIndexComponent.new(
3
3
  resource: @resource,
4
4
  resources: @resources,
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceShowComponent.new(resource: @resource, reflection: @reflection, actions: @actions) %>
3
3
  <% end %>
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_wrap(params[:turbo_frame]) do %>
1
+ <%= render Avo::TurboFrameWrapperComponent.new(params[:turbo_frame]) do %>
2
2
  <%= render Avo::CardComponent.new card: @card %>
3
3
  <% end %>
@@ -14,7 +14,7 @@
14
14
  hq_payload = Avo::Licensing::HQ.new(request).payload
15
15
  %>
16
16
  <div class="flex flex-col">
17
- <%= render Avo::PanelComponent.new title: 'Debug Avo', description: 'Use this page to debug the Avo license.' do |c| %>
17
+ <%= render Avo::PanelComponent.new(title: 'Debug Avo', description: 'Use this page to debug the Avo license.') do |c| %>
18
18
  <% c.tools do %>
19
19
  <% end %>
20
20
  <% c.bare_content do %>
@@ -11,6 +11,6 @@
11
11
  </div>
12
12
  </div>
13
13
 
14
- <a href="https://docs.avohq.io/1.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/2.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>