avo 2.9.2.pre1 → 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 (110) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +5 -7
  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/date_field/edit_component.html.erb +1 -0
  11. data/app/components/avo/fields/date_time_field/edit_component.html.erb +10 -25
  12. data/app/components/avo/fields/date_time_field/index_component.html.erb +1 -9
  13. data/app/components/avo/fields/date_time_field/show_component.html.erb +1 -9
  14. data/app/components/avo/fields/edit_component.rb +5 -0
  15. data/app/components/avo/fields/show_component.rb +1 -1
  16. data/app/components/avo/index/ordering/button_component.rb +2 -12
  17. data/app/components/avo/index/resource_controls_component.html.erb +2 -2
  18. data/app/components/avo/index/resource_controls_component.rb +5 -1
  19. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  20. data/app/components/avo/index/table_row_component.html.erb +1 -1
  21. data/app/components/avo/item_switcher_component.html.erb +19 -0
  22. data/app/components/avo/item_switcher_component.rb +45 -0
  23. data/app/components/avo/panel_component.html.erb +23 -24
  24. data/app/components/avo/panel_component.rb +8 -5
  25. data/app/components/avo/tab_group_component.html.erb +53 -0
  26. data/app/components/avo/tab_group_component.rb +51 -0
  27. data/app/components/avo/tab_switcher_component.html.erb +21 -0
  28. data/app/components/avo/tab_switcher_component.rb +86 -0
  29. data/app/components/avo/views/resource_edit_component.html.erb +34 -56
  30. data/app/components/avo/views/resource_edit_component.rb +11 -1
  31. data/app/components/avo/views/resource_index_component.html.erb +1 -1
  32. data/app/components/avo/views/resource_index_component.rb +3 -3
  33. data/app/components/avo/views/resource_show_component.html.erb +58 -89
  34. data/app/components/avo/views/resource_show_component.rb +2 -2
  35. data/app/controllers/avo/actions_controller.rb +1 -1
  36. data/app/controllers/avo/application_controller.rb +20 -3
  37. data/app/helpers/avo/application_helper.rb +0 -6
  38. data/app/helpers/avo/url_helpers.rb +1 -1
  39. data/app/javascript/avo.js +5 -1
  40. data/app/javascript/js/controllers/fields/date_field_controller.js +25 -87
  41. data/app/javascript/js/controllers/loading_button_controller.js +25 -21
  42. data/app/javascript/js/controllers/tabs_controller.js +86 -0
  43. data/app/javascript/js/controllers.js +2 -0
  44. data/app/views/avo/base/index.html.erb +1 -1
  45. data/app/views/avo/base/show.html.erb +1 -1
  46. data/app/views/avo/cards/show.html.erb +1 -1
  47. data/app/views/avo/debug/index.html.erb +1 -1
  48. data/app/views/avo/home/_actions.html.erb +1 -1
  49. data/app/views/avo/home/_dashboards.html.erb +19 -0
  50. data/app/views/avo/home/_filters.html.erb +1 -1
  51. data/app/views/avo/home/_resources.html.erb +1 -1
  52. data/app/views/avo/home/failed_to_load.html.erb +1 -1
  53. data/app/views/avo/home/index.html.erb +14 -2
  54. data/app/views/avo/partials/_javascript.html.erb +1 -1
  55. data/app/views/avo/partials/_tabs_toggle.html.erb +20 -0
  56. data/app/views/avo/private/design.html.erb +1 -1
  57. data/config/routes.rb +1 -1
  58. data/db/migrate/20210421064037_add_color_to_teams.rb +5 -0
  59. data/db/migrate/20210423075924_add_progress_to_projects.rb +5 -0
  60. data/db/migrate/20210525143134_add_slug_to_users.rb +6 -0
  61. data/lib/avo/app.rb +11 -4
  62. data/lib/avo/base_action.rb +2 -19
  63. data/lib/avo/base_card.rb +1 -7
  64. data/lib/avo/base_resource.rb +1 -95
  65. data/lib/avo/base_resource_tool.rb +3 -1
  66. data/lib/avo/concerns/handles_field_args.rb +1 -1
  67. data/lib/avo/concerns/has_fields.rb +247 -50
  68. data/lib/avo/concerns/has_html_attributes.rb +1 -1
  69. data/lib/avo/concerns/is_resource_item.rb +36 -0
  70. data/lib/avo/concerns/model_class_constantized.rb +23 -0
  71. data/lib/avo/dashboards/base_dashboard.rb +1 -1
  72. data/lib/avo/dsl/field_parser.rb +83 -0
  73. data/lib/avo/fields/base_field.rb +19 -2
  74. data/lib/avo/fields/date_field.rb +2 -0
  75. data/lib/avo/fields/date_time_field.rb +9 -21
  76. data/lib/avo/fields/field_extensions/visible_in_different_views.rb +18 -1
  77. data/lib/avo/fields/has_base_field.rb +20 -1
  78. data/lib/avo/fields/has_one_field.rb +4 -1
  79. data/lib/avo/grid_collector.rb +6 -3
  80. data/lib/avo/items_holder.rb +68 -0
  81. data/lib/avo/licensing/h_q.rb +10 -0
  82. data/lib/avo/main_panel.rb +3 -0
  83. data/lib/avo/menu/builder.rb +8 -7
  84. data/lib/avo/panel.rb +25 -0
  85. data/lib/avo/panel_builder.rb +23 -0
  86. data/lib/avo/services/uri_service.rb +71 -0
  87. data/lib/avo/tab.rb +78 -0
  88. data/lib/avo/tab_builder.rb +25 -0
  89. data/lib/avo/tab_group.rb +40 -0
  90. data/lib/avo/tab_group_builder.rb +43 -0
  91. data/lib/avo/version.rb +1 -1
  92. data/lib/avo.rb +1 -0
  93. data/lib/generators/avo/templates/locales/avo.fr.yml +115 -0
  94. data/lib/generators/avo/templates/resource/controller.tt +2 -0
  95. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  96. data/lib/generators/avo/templates/tool/view.tt +1 -1
  97. data/public/avo-assets/avo.css +27 -3
  98. data/public/avo-assets/avo.js +77 -77
  99. data/public/avo-assets/avo.js.map +3 -3
  100. metadata +28 -13
  101. data/app/assets/builds/action_cable.js +0 -2
  102. data/app/assets/builds/action_cable.js.map +0 -7
  103. data/app/assets/builds/application.js +0 -2
  104. data/app/assets/builds/application.js.map +0 -7
  105. data/app/assets/builds/avo.css +0 -9028
  106. data/app/assets/builds/avo.js +0 -512
  107. data/app/assets/builds/avo.js.map +0 -7
  108. data/app/assets/builds/avo_custom.js +0 -6
  109. data/app/assets/builds/avo_custom.js.map +0 -7
  110. data/lib/avo/concerns/has_tools.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ec2fea3d7eaa7759c9864553df3ed3e2375691e8e77616526a3bdf24a9b7c095
4
- data.tar.gz: f958e3660b85e7f55352ca3680b1cbc6db6f5241a4b67335cc734c32f042c4b6
3
+ metadata.gz: 75872d452d6a4918c39857b6dbc0075477f13a87fa42d6bba3cc1da5fa924be0
4
+ data.tar.gz: e3b24ecd70d7fc005a33fd4b4fd34b79fd1c228db6125be80574968f639677b8
5
5
  SHA512:
6
- metadata.gz: 184f093da1d7adec6414db22ca03f8222103768c35c382c3c0806311665094e02f0573889769f42ff100e3eb82a0f1e3d43a77fd10ad6c22d5357c6ec1a4fdbf
7
- data.tar.gz: 03b309a8cff875d3aa1459a51d1196881ac03eae9935b3a4edf152125216bacbfbc0d468afdeb0e1f4da7998adcb468c0639615e396b4f5554e1f597b6f67ee2
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.2.pre1)
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)
@@ -252,8 +252,6 @@ GEM
252
252
  nokogiri (1.13.4)
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
@@ -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 {
@@ -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
@@ -6,6 +6,7 @@
6
6
  'date-field-target': 'input',
7
7
  'first-day-of-week': @field.first_day_of_week,
8
8
  'picker-format': @field.picker_format,
9
+ 'disable-mobile': @field.disable_mobile,
9
10
  'enable-time': false,
10
11
  format: @field.format,
11
12
  placeholder: @field.placeholder,
@@ -1,31 +1,16 @@
1
1
  <%= edit_field_wrapper field: @field, index: @index, form: @form, resource: @resource, displayed_in_modal: @displayed_in_modal do %>
2
- <%= content_tag :div, data: {
3
- controller: "date-field",
4
- date_field_view_value: @view,
5
- date_field_enable_time_value: true,
6
- date_field_picker_format_value: @field.picker_format,
7
- date_field_first_day_of_week_value: @field.first_day_of_week,
8
- date_field_time24_hr_value: @field.time_24hr,
9
- date_field_timezone_value: @field.timezone,
10
- } do %>
11
- <%= datetime_field "fake_#{@field.id}", "fake",
12
- value: @field.edit_formatted_value,
2
+ <div data-controller="date-field">
3
+ <%= @form.datetime_field @field.id,
13
4
  class: classes("w-full"),
14
- data: {
15
- 'date-field-target': 'fakeInput',
16
- placeholder: @field.placeholder,
17
- relative: @field.relative,
18
- **@field.get_html(:data, view: view, element: :input)
19
- },
20
- disabled: @field.readonly,
21
- placeholder: @field.placeholder,
22
- style: @field.get_html(:style, view: view, element: :input)
23
- %>
24
- <%= @form.text_field @field.id,
25
- value: @field.edit_formatted_value,
26
- class: classes("w-full hidden"),
27
5
  data: {
28
6
  'date-field-target': 'input',
7
+ 'first-day-of-week': @field.first_day_of_week,
8
+ 'picker-format': @field.picker_format,
9
+ 'disable-mobile': @field.disable_mobile,
10
+ 'enable-time': true,
11
+ time24hr: @field.time_24hr,
12
+ timezone: @field.timezone,
13
+ format: @field.format,
29
14
  placeholder: @field.placeholder,
30
15
  relative: @field.relative,
31
16
  **@field.get_html(:data, view: view, element: :input)
@@ -34,5 +19,5 @@
34
19
  placeholder: @field.placeholder,
35
20
  style: @field.get_html(:style, view: view, element: :input)
36
21
  %>
37
- <% end %>
22
+ </div>
38
23
  <% end %>
@@ -1,11 +1,3 @@
1
1
  <%= index_field_wrapper field: @field, resource: @resource do %>
2
- <%= content_tag :div, data: {
3
- controller: "date-field",
4
- date_field_view_value: @view,
5
- date_field_format_value: @field.format,
6
- date_field_timezone_value: @field.timezone,
7
- date_field_picker_format_value: @field.picker_format,
8
- } do %>
9
- <%= @field.formatted_value %>
10
- <% end %>
2
+ <%= @field.formatted_value %>
11
3
  <% end %>
@@ -1,11 +1,3 @@
1
1
  <%= show_field_wrapper field: @field, resource: @resource, index: @index do %>
2
- <%= content_tag :div, data: {
3
- controller: "date-field",
4
- date_field_view_value: @view,
5
- date_field_format_value: @field.format,
6
- date_field_timezone_value: @field.timezone,
7
- date_field_picker_format_value: @field.picker_format,
8
- } do %>
9
- <%= @field.formatted_value %>
10
- <% end %>
2
+ <%= @field.formatted_value %>
11
3
  <% end %>
@@ -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