avo 3.0.1.beta24 → 3.0.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 (93) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +5 -5
  3. data/Gemfile.lock +164 -128
  4. data/README.md +19 -27
  5. data/app/components/avo/actions_component.rb +2 -2
  6. data/app/components/avo/fields/belongs_to_field/edit_component.html.erb +122 -104
  7. data/app/components/avo/fields/belongs_to_field/edit_component.rb +11 -0
  8. data/app/components/avo/fields/location_field/edit_component.html.erb +12 -10
  9. data/app/components/avo/index/grid_item_component.html.erb +1 -1
  10. data/app/components/avo/index/resource_table_component.html.erb +2 -2
  11. data/app/components/avo/index/resource_table_component.rb +16 -0
  12. data/app/components/avo/index/table_row_component.html.erb +1 -1
  13. data/app/components/avo/items/panel_component.html.erb +25 -0
  14. data/app/components/avo/items/panel_component.rb +43 -0
  15. data/app/components/avo/items/switcher_component.html.erb +17 -0
  16. data/app/components/avo/items/switcher_component.rb +79 -0
  17. data/app/components/avo/items/visible_items_component.html.erb +10 -0
  18. data/app/components/avo/items/visible_items_component.rb +11 -0
  19. data/app/components/avo/modal_component.html.erb +11 -7
  20. data/app/components/avo/modal_component.rb +21 -0
  21. data/app/components/avo/paginator_component.html.erb +29 -40
  22. data/app/components/avo/paginator_component.rb +18 -0
  23. data/app/components/avo/panel_component.html.erb +2 -2
  24. data/app/components/avo/referrer_params_component.html.erb +1 -0
  25. data/app/components/avo/resource_component.rb +8 -15
  26. data/app/components/avo/resource_sidebar_component.html.erb +11 -18
  27. data/app/components/avo/resource_sidebar_component.rb +4 -2
  28. data/app/components/avo/sidebar_profile_component.html.erb +1 -1
  29. data/app/components/avo/tab_group_component.html.erb +1 -1
  30. data/app/components/avo/views/resource_edit_component.html.erb +15 -32
  31. data/app/components/avo/views/resource_edit_component.rb +18 -6
  32. data/app/components/avo/views/resource_index_component.rb +1 -0
  33. data/app/components/avo/views/resource_show_component.html.erb +14 -33
  34. data/app/components/avo/views/resource_show_component.rb +11 -0
  35. data/app/controllers/avo/actions_controller.rb +6 -4
  36. data/app/controllers/avo/application_controller.rb +2 -33
  37. data/app/controllers/avo/base_controller.rb +25 -16
  38. data/app/helpers/avo/application_helper.rb +4 -0
  39. data/app/javascript/js/controllers/fields/reload_belongs_to_field_controller.js +51 -0
  40. data/app/javascript/js/controllers.js +2 -0
  41. data/app/views/avo/actions/show.html.erb +4 -3
  42. data/app/views/avo/base/_new_via_belongs_to.html.erb +12 -0
  43. data/app/views/avo/base/close_modal_and_reload_field.turbo_stream.erb +8 -0
  44. data/app/views/avo/base/create_fail_action.turbo_stream.erb +13 -0
  45. data/app/views/avo/partials/_flash_alerts.turbo_stream.erb +3 -0
  46. data/config/i18n-tasks.yml +1 -1
  47. data/config/initializers/pagy.rb +2 -0
  48. data/config/master.key +1 -0
  49. data/lib/avo/base_action.rb +44 -38
  50. data/lib/avo/base_resource.rb +7 -8
  51. data/lib/avo/concerns/borrow_items_holder.rb +35 -0
  52. data/lib/avo/concerns/has_items.rb +65 -47
  53. data/lib/avo/concerns/hydration.rb +17 -0
  54. data/lib/avo/concerns/is_resource_item.rb +2 -10
  55. data/lib/avo/concerns/pagination.rb +53 -0
  56. data/lib/avo/concerns/visible_items.rb +7 -30
  57. data/lib/avo/dsl/field_parser.rb +1 -1
  58. data/lib/avo/fields/concerns/is_searchable.rb +3 -1
  59. data/lib/avo/fields/location_field.rb +2 -2
  60. data/lib/avo/filters/base_filter.rb +1 -0
  61. data/lib/avo/html/builder.rb +1 -0
  62. data/lib/avo/licensing/h_q.rb +4 -6
  63. data/lib/avo/licensing/license.rb +4 -0
  64. data/lib/avo/plugin.rb +5 -1
  65. data/lib/avo/resources/items/holder.rb +29 -11
  66. data/lib/avo/resources/items/item_group.rb +4 -9
  67. data/lib/avo/resources/items/main_panel.rb +10 -0
  68. data/lib/avo/resources/items/row.rb +4 -16
  69. data/lib/avo/resources/items/sidebar.rb +9 -10
  70. data/lib/avo/resources/items/tab.rb +3 -9
  71. data/lib/avo/resources/items/tab_group.rb +6 -12
  72. data/lib/avo/version.rb +1 -1
  73. data/lib/generators/avo/templates/locales/avo.ar.yml +7 -6
  74. data/lib/generators/avo/templates/locales/avo.en.yml +2 -0
  75. data/lib/generators/avo/templates/locales/avo.es.yml +127 -0
  76. data/lib/generators/avo/templates/locales/avo.fr.yml +2 -0
  77. data/lib/generators/avo/templates/locales/avo.nb.yml +2 -0
  78. data/lib/generators/avo/templates/locales/avo.nn.yml +2 -0
  79. data/lib/generators/avo/templates/locales/avo.pt-BR.yml +2 -0
  80. data/lib/generators/avo/templates/locales/avo.pt.yml +2 -0
  81. data/lib/generators/avo/templates/locales/avo.ro.yml +2 -0
  82. data/lib/generators/avo/templates/locales/avo.tr.yml +2 -0
  83. data/public/avo-assets/avo.base.css +121 -4
  84. data/public/avo-assets/avo.base.js +86 -86
  85. data/public/avo-assets/avo.base.js.map +3 -3
  86. data/public/avo-assets/avo.css +9744 -0
  87. data/public/avo-assets/avo.js +513 -0
  88. data/public/avo-assets/avo.js.map +7 -0
  89. metadata +23 -8
  90. data/app/components/avo/item_switcher_component.html.erb +0 -27
  91. data/app/components/avo/item_switcher_component.rb +0 -48
  92. data/app/views/avo/actions/keep_modal_open.turbo_stream.erb +0 -5
  93. data/lib/avo/action_model.rb +0 -20
@@ -129,6 +129,10 @@ module Avo
129
129
  })
130
130
  end
131
131
 
132
+ def frame_id(resource)
133
+ ["frame", resource.model_name.singular, resource.record.id].compact.join("-")
134
+ end
135
+
132
136
  private
133
137
 
134
138
  # Taken from the original library
@@ -0,0 +1,51 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+
3
+ // Used as a custom Stream Action <turbo-stream action="update-belongs-to" />
4
+ export default class extends Controller {
5
+ static values = {
6
+ polymorphic: Boolean,
7
+ searchable: Boolean,
8
+ targetName: String,
9
+ relationName: String,
10
+ }
11
+
12
+ beforeStreamRender(event) {
13
+ const { relationName } = event.target.dataset
14
+ if (event.target.action !== 'update-belongs-to' || this.relationNameValue !== relationName) {
15
+ return false
16
+ }
17
+
18
+ event.detail.render = (stream) => {
19
+ if (this.searchableValue) {
20
+ this.updateSearchable(stream)
21
+ } else {
22
+ this.updateNonSearchable(stream)
23
+ }
24
+ }
25
+ }
26
+
27
+ updateSearchable(stream) {
28
+ // Update the id component
29
+ document.querySelector(`input[name="${this.targetNameValue}"][type="hidden"]`).value = stream.dataset.targetRecordId
30
+ // Update the label
31
+ document.querySelector(`input[name="${this.targetNameValue}"][type="text"]`).value = stream.dataset.targetResourceLabel
32
+ }
33
+
34
+ updateNonSearchable(stream) {
35
+ const select = this.selectorContext(stream).querySelector(`select[name="${this.targetNameValue}"]`)
36
+ const option = document.createElement('option')
37
+ option.value = stream.dataset.targetRecordId
38
+ option.text = stream.dataset.targetResourceLabel
39
+ option.selected = true
40
+ select.appendChild(option)
41
+ }
42
+
43
+ selectorContext(stream) {
44
+ // if polymorphic, search for the select in the correct sub-container
45
+ if (this.polymorphicValue) {
46
+ return document.querySelector(`[data-type="${stream.dataset.targetResourceClass}"]`)
47
+ }
48
+
49
+ return document
50
+ }
51
+ }
@@ -24,6 +24,7 @@ import MultipleSelectFilterController from './controllers/multiple_select_filter
24
24
  import PerPageController from './controllers/per_page_controller'
25
25
  import PreviewController from './controllers/preview_controller'
26
26
  import ProgressBarFieldController from './controllers/fields/progress_bar_field_controller'
27
+ import ReloadBelongsToFieldController from './controllers/fields/reload_belongs_to_field_controller'
27
28
  import ResourceEditController from './controllers/resource_edit_controller'
28
29
  import ResourceIndexController from './controllers/resource_index_controller'
29
30
  import ResourceShowController from './controllers/resource_show_controller'
@@ -76,6 +77,7 @@ application.register('date-field', DateFieldController)
76
77
  application.register('easy-mde', EasyMdeController)
77
78
  application.register('key-value', KeyValueController)
78
79
  application.register('progress-bar-field', ProgressBarFieldController)
80
+ application.register('reload-belongs-to-field', ReloadBelongsToFieldController)
79
81
  application.register('trix-field', TrixFieldController)
80
82
 
81
83
  // Custom controllers
@@ -7,8 +7,7 @@
7
7
  data-resource-id="<%= params[:id] %>"
8
8
  class="hidden text-slate-800"
9
9
  >
10
- <%= form_with model: @record,
11
- scope: 'fields',
10
+ <%= form_with scope: 'fields',
12
11
  url: Avo::Services::URIService.parse(@resource.records_path).append_paths("actions").to_s,
13
12
  local: true,
14
13
  data: @action.class.form_data_attributes do |form|
@@ -17,10 +16,11 @@
17
16
  <% c.with_heading do %>
18
17
  <%= @action.action_name %>
19
18
  <% end %>
19
+
20
20
  <div class="flex-1 flex">
21
21
  <%= @action.get_message %>
22
22
  </div>
23
- <%= hidden_field_tag :action_id, @action.param_id %>
23
+ <%= hidden_field_tag :action_id, @action.to_param %>
24
24
  <%= form.hidden_field :avo_resource_ids, value: params[:id] || params[:resource_ids], 'data-action-target': 'resourceIds' %>
25
25
  <%= form.hidden_field :avo_selected_query, 'data-action-target': 'selectedAllQuery' %>
26
26
  <%= form.hidden_field :arguments, value: params[:arguments] %>
@@ -35,6 +35,7 @@
35
35
  <% end %>
36
36
  </div>
37
37
  <% end %>
38
+
38
39
  <% c.with_controls do %>
39
40
  <%= a_button type: :button,
40
41
  data: { action: 'click->modal#close' },
@@ -0,0 +1,12 @@
1
+ <%= turbo_frame_tag "new_via_belongs_to" do %>
2
+ <%= render(Avo::ModalComponent.new(width: :xl, body_class: "bg-application")) do |c| %>
3
+ <div class="pt-4 pb-8">
4
+ <%= render Avo::Views::ResourceEditComponent.new(
5
+ resource: @resource,
6
+ record: @record,
7
+ view: @view,
8
+ display_breadcrumbs: false
9
+ ) %>
10
+ </div>
11
+ <% end %>
12
+ <% end %>
@@ -0,0 +1,8 @@
1
+ <%= turbo_stream.remove("new_via_belongs_to") %>
2
+
3
+ <turbo-stream action="update-belongs-to"
4
+ data-relation-name="<%= params[:via_relation] %>"
5
+ data-target-record-id="<%= @record.id %>"
6
+ data-target-resource-label="<%= @resource.record_title %>"
7
+ data-target-resource-class="<%= @record.class.name %>">
8
+ </turbo-stream>
@@ -0,0 +1,13 @@
1
+ <%= turbo_stream.replace(frame_id(@resource), template: "avo/base/new") %>
2
+
3
+ <%= turbo_stream.append "alerts" do %>
4
+ <%= render Avo::FlashAlertsComponent.new flashes: flash %>
5
+ <% end %>
6
+
7
+ <% if @record.errors.any? %>
8
+ <%= turbo_stream.append("alerts") do %>
9
+ <% @record.errors.full_messages.each do |message| %>
10
+ <%= render Avo::AlertComponent.new :error, message %>
11
+ <% end %>
12
+ <% end %>
13
+ <% end %>
@@ -0,0 +1,3 @@
1
+ <%= turbo_stream.append "alerts" do %>
2
+ <%= render Avo::FlashAlertsComponent.new flashes: flash.discard %>
3
+ <% end %>
@@ -3,7 +3,7 @@
3
3
  # The "main" locale.
4
4
  base_locale: en
5
5
  ## All available locales are inferred from the data by default. Alternatively, specify them explicitly:
6
- locales: [en, fr, nb, nn, pt-BR, pt, ro, tr]
6
+ locales: [ar, en, es, fr, nb, nn, pt-BR, pt, ro, tr]
7
7
  ## Reporting locale, default: en. Available: en, ru.
8
8
  # internal_locale: en
9
9
 
@@ -1,4 +1,5 @@
1
1
  require "pagy/extras/trim"
2
+ require "pagy/extras/countless"
2
3
 
3
4
  # For locales without native pagy i18n support
4
5
  def pagy_locale_path(file_name)
@@ -7,6 +8,7 @@ end
7
8
 
8
9
  extra_locales = [
9
10
  {locale: "en"},
11
+ {locale: "es"},
10
12
  {locale: "fr"},
11
13
  {locale: "nb"},
12
14
  {locale: "pt-BR"},
data/config/master.key ADDED
@@ -0,0 +1 @@
1
+ 2aeb23d82b909d9c6b5abb62f7058c2a
@@ -7,22 +7,19 @@ module Avo
7
7
  class_attribute :confirm_button_label
8
8
  class_attribute :cancel_button_label
9
9
  class_attribute :no_confirmation, default: false
10
- class_attribute :record
11
- class_attribute :view
12
- class_attribute :user
13
- class_attribute :resource
14
10
  class_attribute :standalone, default: false
15
11
  class_attribute :visible
16
12
  class_attribute :may_download_file, default: false
17
13
  class_attribute :turbo
14
+ class_attribute :authorize, default: true
18
15
 
16
+ attr_accessor :view
19
17
  attr_accessor :response
20
18
  attr_accessor :record
21
19
  attr_accessor :resource
22
20
  attr_accessor :user
23
21
  attr_reader :arguments
24
22
 
25
- delegate :view, to: :class
26
23
  # TODO: find a differnet way to delegate this to the uninitialized Current variable
27
24
  delegate :context, to: Avo::Current
28
25
  def current_user
@@ -32,6 +29,7 @@ module Avo
32
29
  delegate :view_context, to: Avo::Current
33
30
  delegate :avo, to: :view_context
34
31
  delegate :main_app, to: :view_context
32
+ delegate :to_param, to: :class
35
33
 
36
34
  class << self
37
35
  delegate :context, to: ::Avo::Current
@@ -53,6 +51,23 @@ module Avo
53
51
  {}
54
52
  end
55
53
  end
54
+
55
+ def to_param
56
+ to_s
57
+ end
58
+
59
+ def link_arguments(resource:, **args)
60
+ path = Avo::Services::URIService.parse(resource.record.present? ? resource.record_path : resource.records_path)
61
+ .append_paths("actions")
62
+ .append_query(action_id: to_param, **args)
63
+ .to_s
64
+
65
+ data = {
66
+ turbo_frame: "actions_show",
67
+ }
68
+
69
+ [path, data]
70
+ end
56
71
  end
57
72
 
58
73
  def action_name
@@ -62,11 +77,15 @@ module Avo
62
77
  end
63
78
 
64
79
  def initialize(record: nil, resource: nil, user: nil, view: nil, arguments: {})
65
- self.class.record = record
66
- self.class.resource = resource
67
- self.class.user = user
68
- self.class.view = Avo::ViewInquirer.new(view)
69
- @arguments = arguments
80
+ @record = record
81
+ @resource = resource
82
+ @user = user
83
+ @view = Avo::ViewInquirer.new(view)
84
+ @arguments = Avo::ExecutionContext.new(
85
+ target: arguments,
86
+ resource: resource,
87
+ record: record
88
+ ).handle.with_indifferent_access
70
89
 
71
90
  self.class.message ||= I18n.t("avo.are_you_sure_you_want_to_run_this_option")
72
91
  self.class.confirm_button_label ||= I18n.t("avo.run")
@@ -84,26 +103,17 @@ module Avo
84
103
  end
85
104
 
86
105
  def get_message
87
- Avo::ExecutionContext.new(target: self.class.message, record: self.class.record, resource: self.class.resource).handle
88
- end
89
-
90
- def get_attributes_for_action
91
- get_fields.map do |field|
92
- value = field.value || Avo::ExecutionContext.new(
93
- target: field.default,
94
- record: self.class.record,
95
- resource: self.class.resource,
96
- view: view
97
- ).handle
98
-
99
- [field.id, value]
100
- end.to_h
106
+ Avo::ExecutionContext.new(target: self.class.message, record: @record, resource: @resource).handle
101
107
  end
102
108
 
103
109
  def handle_action(**args)
104
110
  processed_fields = if args[:fields].present?
105
111
  # Fetching the field definitions and not the actual fields (get_fields) because they will break if the user uses a `visible` block and adds a condition using the `params` variable. The params are different in the show method and the handle method.
106
- action_fields = get_field_definitions.map { |field| [field.id, field] }.to_h
112
+ action_fields = get_field_definitions.map do |field|
113
+ field.hydrate(resource: @resource)
114
+
115
+ [field.id, field]
116
+ end.to_h
107
117
 
108
118
  # For some fields, like belongs_to, the id and database_id differ (user vs user_id).
109
119
  # That's why we need to fetch the database_id for when we process the action.
@@ -134,6 +144,8 @@ module Avo
134
144
  end
135
145
 
136
146
  def visible_in_view(parent_resource: nil)
147
+ return false unless authorized?
148
+
137
149
  if visible.blank?
138
150
  # Hide on the :new view by default
139
151
  return false if view.new?
@@ -147,16 +159,12 @@ module Avo
147
159
  target: visible,
148
160
  params: params,
149
161
  parent_resource: parent_resource,
150
- resource: self.class.resource,
151
- view: self.class.view,
162
+ resource: @resource,
163
+ view: @view,
152
164
  arguments: arguments
153
165
  ).handle
154
166
  end
155
167
 
156
- def param_id
157
- self.class.to_s
158
- end
159
-
160
168
  def succeed(text)
161
169
  add_message text, :success
162
170
 
@@ -220,13 +228,11 @@ module Avo
220
228
  self
221
229
  end
222
230
 
223
- # We're overriding this method to hydrate with the proper resource attribute.
224
- def hydrate_fields
225
- fields.map do |field|
226
- field.hydrate(record: @record, view: @view, resource: resource)
227
- end
228
-
229
- self
231
+ def authorized?
232
+ Avo::ExecutionContext.new(
233
+ target: authorize,
234
+ action: self
235
+ ).handle
230
236
  end
231
237
 
232
238
  private
@@ -10,6 +10,8 @@ module Avo
10
10
  include Avo::Concerns::ModelClassConstantized
11
11
  include Avo::Concerns::HasDescription
12
12
  include Avo::Concerns::HasHelpers
13
+ include Avo::Concerns::Hydration
14
+ include Avo::Concerns::Pagination
13
15
 
14
16
  # Avo::Current methods
15
17
  delegate :context, to: Avo::Current
@@ -232,6 +234,7 @@ module Avo
232
234
  delegate :underscore_name, to: :class
233
235
  delegate :find_record, to: :class
234
236
  delegate :model_key, to: :class
237
+ delegate :tab, to: :items_holder
235
238
 
236
239
  def initialize(record: nil, view: nil, user: nil, params: nil)
237
240
  @view = Avo::ViewInquirer.new(view) if view.present?
@@ -252,7 +255,7 @@ module Avo
252
255
  end
253
256
 
254
257
  def detect_fields
255
- self.items_holder = Avo::Resources::Items::Holder.new
258
+ self.items_holder = Avo::Resources::Items::Holder.new(parent: self)
256
259
 
257
260
  # Used in testing to replace items
258
261
  if temporary_items.present?
@@ -321,14 +324,10 @@ module Avo
321
324
  end
322
325
  end
323
326
 
324
- def hydrate(record: nil, view: nil, user: nil, params: nil)
325
- @view = Avo::ViewInquirer.new(view) if view.present?
326
- @user = user if user.present?
327
- @params = params if params.present?
328
-
329
- if record.present?
330
- @record = record
327
+ def hydrate(...)
328
+ super(...)
331
329
 
330
+ if @record.present?
332
331
  hydrate_model_with_default_values if @view&.new?
333
332
  end
334
333
 
@@ -0,0 +1,35 @@
1
+ module Avo
2
+ module Concerns
3
+ module BorrowItemsHolder
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ attr_reader :items_holder
8
+ end
9
+
10
+ class_methods do
11
+ def parse_block(parent:, **args, &block)
12
+ # item = Avo::Resources::Items:: ...
13
+ item = new(parent: parent, **args)
14
+
15
+ # Borrow the current items holder to the parent (parent = Action || Resource, etc.)
16
+ # Save parent's items holder to restore it after the block is parsed
17
+ # This is useful when you execute parent's methods like `some_fields_method` inside some DSL block.
18
+ # When you do that, Docile will not find the method in the current object (item), but in the parent.
19
+ # So we need to temporarily replace the parent's items holder with the current one because the parent's methods
20
+ # will be executed in the parent's context.
21
+ # For more context: https://github.com/ms-ati/docile/issues/107
22
+ parent_item_holder = parent.items_holder
23
+ parent.items_holder = item.items_holder
24
+
25
+ dsl_evaluation = Docile.dsl_eval(item, &block).build
26
+
27
+ # Restore the parent's items holder
28
+ parent.items_holder = parent_item_holder
29
+
30
+ dsl_evaluation
31
+ end
32
+ end
33
+ end
34
+ end
35
+ end
@@ -47,6 +47,7 @@ module Avo
47
47
  delegate :tool, to: :items_holder
48
48
  delegate :heading, to: :items_holder
49
49
  delegate :sidebar, to: :items_holder
50
+ delegate :main_panel, to: :items_holder
50
51
 
51
52
  def items_holder
52
53
  @items_holder || Avo::Resources::Items::Holder.new
@@ -57,7 +58,15 @@ module Avo
57
58
  # end
58
59
 
59
60
  def invalid_fields
60
- items_holder.invalid_fields
61
+ invalid_fields = items_holder.invalid_fields
62
+
63
+ items_holder.items.each do |item|
64
+ if item.respond_to? :items
65
+ invalid_fields += item.invalid_fields
66
+ end
67
+ end
68
+
69
+ invalid_fields
61
70
  end
62
71
 
63
72
  def fields(**args)
@@ -101,6 +110,10 @@ module Avo
101
110
  if item.is_row?
102
111
  fields << extract_fields_from_items(tab)
103
112
  end
113
+
114
+ if item.is_main_panel?
115
+ fields << extract_fields_from_items(item)
116
+ end
104
117
  end
105
118
 
106
119
  fields.flatten
@@ -194,65 +207,56 @@ module Avo
194
207
  end
195
208
  end
196
209
 
197
- # Separates the fields that are in a panel and those that are just hanging out.
198
- # Take the ones that aren't placed into a panel and add them to the "default" panel.
199
- # This is to keep compatibility with the versions before 2.10 when you didn't have the ability to add fields to panels.
200
210
  def get_items
201
- panelless_items = []
202
- panelfull_items = []
203
-
204
- visible_items
205
- .each do |item|
206
- if item.is_field?
207
- if item.has_own_panel?
208
- panelfull_items << item
209
- else
210
- panelless_items << item
211
- end
212
- else
213
- panelfull_items << item
214
- end
215
- end
216
-
217
- # Make sure all tabs panelfull_items are setted as inside tabs
218
- panelfull_items.grep(Avo::Resources::Items::TabGroup).each do |tab_group|
219
- tab_group.items.grep(Avo::Resources::Items::Tab).each do |tab|
220
- tab.items.grep(Avo::Resources::Items::Panel).each do |panel|
221
- set_target_to_top panel.items.grep(Avo::Fields::BelongsToField)
211
+ # Each group is built only by standalone items or items that have their own panel, keeping the items order
212
+ grouped_items = visible_items.slice_when do |prev, curr|
213
+ # Slice when the item type changes from standalone to panel or vice-versa
214
+ is_standalone?(prev) != is_standalone?(curr)
215
+ end.to_a.map do |group|
216
+ { elements: group, is_standalone: is_standalone?(group.first) }
217
+ end
222
218
 
223
- panel.items.grep(Avo::Resources::Items::Row).each do |row|
224
- set_target_to_top row.items.grep(Avo::Fields::BelongsToField)
225
- end
226
- end
219
+ # Creates a main panel if it's missing and adds first standalone group of items if present
220
+ if items.none? { |item| item.is_main_panel? }
221
+ if (standalone_group = grouped_items.find { |group| group[:is_standalone] }).present?
222
+ calculated_main_panel = Avo::Resources::Items::MainPanel.new
223
+ hydrate_item calculated_main_panel
224
+ calculated_main_panel.items_holder.items = standalone_group[:elements]
225
+ grouped_items[grouped_items.index standalone_group] = { elements: [calculated_main_panel], is_standalone: false }
227
226
  end
228
227
  end
229
228
 
230
- # Add all the panelles fields to a new panel
231
- main_panel_holder = Avo::Resources::Items::Holder.new
232
- main_panel_holder.items = panelless_items
233
-
234
- # Add that panel to the main panel
235
- main_panel = Avo::Resources::Items::MainPanel.new
236
- main_panel.items_holder = main_panel_holder
229
+ # For each standalone group, wrap items in a panel
230
+ grouped_items.select { |group| group[:is_standalone] }.each do |group|
231
+ calculated_panel = Avo::Resources::Items::Panel.new
232
+ calculated_panel.items_holder.items = group[:elements]
233
+ hydrate_item calculated_panel
234
+ group[:elements] = calculated_panel
235
+ end
237
236
 
238
- # Return all the items but this time with all the panelless ones inside the main panel
239
- [main_panel, *panelfull_items]
237
+ grouped_items.flat_map { |group| group[:elements] }
240
238
  end
241
239
 
242
240
  def items
243
- if items_holder.present?
244
- items_holder.items
245
- else
246
- []
247
- end
241
+ items_holder&.items || []
248
242
  end
249
243
 
250
244
  def visible_items
251
245
  items
252
246
  .map do |item|
253
- if view.present?
254
- res = self.class.ancestors.include?(Avo::BaseResource) ? self : resource
255
- item.hydrate(view: view, resource: res)
247
+ hydrate_item item
248
+
249
+ if item.is_a? Avo::Resources::Items::TabGroup
250
+ # Set the target to _top for all belongs_to fields in the tab group
251
+ item.items.grep(Avo::Resources::Items::Tab).each do |tab|
252
+ tab.items.grep(Avo::Resources::Items::Panel).each do |panel|
253
+ set_target_to_top panel.items.grep(Avo::Fields::BelongsToField)
254
+
255
+ panel.items.grep(Avo::Resources::Items::Row).each do |row|
256
+ set_target_to_top row.items.grep(Avo::Fields::BelongsToField)
257
+ end
258
+ end
259
+ end
256
260
  end
257
261
 
258
262
  item
@@ -288,7 +292,9 @@ module Avo
288
292
  true
289
293
  end
290
294
  end
291
- .compact
295
+ .select do |item|
296
+ !item.is_a?(Avo::Resources::Items::Sidebar)
297
+ end.compact
292
298
  end
293
299
 
294
300
  def is_empty?
@@ -316,6 +322,18 @@ module Avo
316
322
 
317
323
  fields
318
324
  end
325
+
326
+ # Standalone items are fields that don't have their own panel
327
+ def is_standalone?(item)
328
+ item.is_field? && !item.has_own_panel?
329
+ end
330
+
331
+ def hydrate_item(item)
332
+ return unless item.respond_to? :hydrate
333
+
334
+ res = self.class.ancestors.include?(Avo::BaseResource) ? self : resource
335
+ item.hydrate(view: view, resource: res)
336
+ end
319
337
  end
320
338
  end
321
339
  end
@@ -0,0 +1,17 @@
1
+ module Avo
2
+ module Concerns
3
+ module Hydration
4
+ extend ActiveSupport::Concern
5
+
6
+ def hydrate(**args)
7
+ args.each do |key, value|
8
+ value = Avo::ViewInquirer.new value if key == :view
9
+
10
+ send("#{key}=", value) if respond_to?("#{key}=")
11
+ end
12
+
13
+ self
14
+ end
15
+ end
16
+ end
17
+ end
@@ -2,20 +2,12 @@
2
2
  module Avo
3
3
  module Concerns
4
4
  module IsResourceItem
5
+ include Avo::Concerns::Hydration
6
+
5
7
  # These attributes are required to be hydrated in order to properly find the visible_items
6
8
  attr_accessor :resource
7
9
  attr_accessor :view
8
10
 
9
- def hydrate(**args)
10
- args.each do |key, value|
11
- if respond_to?("#{key}=")
12
- send("#{key}=", value)
13
- end
14
- end
15
-
16
- self
17
- end
18
-
19
11
  # Returns the final state of if an item is visible or not
20
12
  # For items that have children it checks to see if it contains any visible children.
21
13
  def visible?
@@ -0,0 +1,53 @@
1
+ module Avo
2
+ module Concerns
3
+ module Pagination
4
+ extend ActiveSupport::Concern
5
+
6
+ included do
7
+ include Pagy::Backend
8
+
9
+ class_attribute :pagination, default: {}
10
+
11
+ PAGINATION_METHOD = {
12
+ default: :pagy,
13
+ countless: :pagy_countless,
14
+ } unless defined? PAGINATION_METHOD
15
+
16
+ PAGINATION_DEFAULTS = {
17
+ type: :default,
18
+ size: [1, 2, 2, 1],
19
+ } unless defined? PAGINATION_DEFAULTS
20
+ end
21
+
22
+ def pagination_type
23
+ @pagination_type ||= ActiveSupport::StringInquirer.new(pagination_hash[:type].to_s)
24
+ end
25
+
26
+ def apply_pagination(index_params:, query:)
27
+ extra_pagy_params = {}
28
+
29
+ # Reset open filters when a user navigates to a new page
30
+ extra_pagy_params[:keep_filters_panel_open] = if params[:keep_filters_panel_open] == "1"
31
+ "0"
32
+ end
33
+
34
+ send PAGINATION_METHOD[pagination_type.to_sym],
35
+ query,
36
+ items: index_params[:per_page],
37
+ link_extra: "data-turbo-frame=\"#{params[:turbo_frame]}\"",
38
+ params: extra_pagy_params,
39
+ size: pagination_hash[:size]
40
+ end
41
+
42
+ private
43
+
44
+ def pagination_hash
45
+ @pagination ||= PAGINATION_DEFAULTS.merge Avo::ExecutionContext.new(
46
+ target: pagination,
47
+ resource: self,
48
+ view: @view
49
+ ).handle
50
+ end
51
+ end
52
+ end
53
+ end