avo 2.11.3.pre.3 → 2.13.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 (91) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +2 -4
  3. data/README.md +2 -1
  4. data/app/components/avo/actions_component.html.erb +3 -3
  5. data/app/components/avo/actions_component.rb +12 -3
  6. data/app/components/avo/button_component.rb +1 -3
  7. data/app/components/avo/card_component.html.erb +8 -8
  8. data/app/components/avo/fields/common/gravatar_viewer_component.html.erb +1 -1
  9. data/app/components/avo/fields/date_field/edit_component.html.erb +0 -3
  10. data/app/components/avo/fields/date_time_field/edit_component.html.erb +0 -2
  11. data/app/components/avo/fields/has_one_field/show_component.html.erb +1 -1
  12. data/app/components/avo/fields/tags_field/show_component.rb +0 -2
  13. data/app/components/avo/index/ordering/buttons_component.html.erb +1 -1
  14. data/app/components/avo/index/resource_controls_component.rb +1 -1
  15. data/app/components/avo/item_switcher_component.html.erb +1 -1
  16. data/app/components/avo/panel_component.html.erb +1 -1
  17. data/app/components/avo/panel_component.rb +3 -4
  18. data/app/components/avo/resource_component.rb +6 -4
  19. data/app/components/avo/tab_group_component.html.erb +1 -1
  20. data/app/components/avo/tab_switcher_component.rb +2 -2
  21. data/app/components/avo/views/resource_edit_component.html.erb +1 -1
  22. data/app/components/avo/views/resource_edit_component.rb +1 -21
  23. data/app/components/avo/views/resource_index_component.html.erb +49 -97
  24. data/app/components/avo/views/resource_index_component.rb +19 -0
  25. data/app/components/avo/views/resource_show_component.html.erb +139 -53
  26. data/app/components/avo/views/resource_show_component.rb +1 -0
  27. data/app/controllers/avo/application_controller.rb +7 -3
  28. data/app/controllers/avo/associations_controller.rb +2 -2
  29. data/app/controllers/avo/base_controller.rb +11 -18
  30. data/app/controllers/avo/cards_controller.rb +5 -18
  31. data/app/controllers/avo/dashboards_controller.rb +2 -4
  32. data/app/controllers/avo/search_controller.rb +57 -20
  33. data/app/javascript/js/controllers/{card_controller.js → dashboard_card_controller.js} +0 -0
  34. data/app/javascript/js/controllers/menu_controller.js +2 -2
  35. data/app/javascript/js/controllers/search_controller.js +42 -15
  36. data/app/javascript/js/controllers.js +2 -2
  37. data/app/views/avo/associations/new.html.erb +1 -0
  38. data/app/views/avo/cards/_metric_card.html.erb +2 -2
  39. data/app/views/avo/dashboards/show.html.erb +20 -3
  40. data/app/views/avo/debug/index.html.erb +1 -1
  41. data/app/views/avo/debug/report.html.erb +1 -0
  42. data/app/views/avo/home/index.html.erb +1 -1
  43. data/app/views/avo/partials/_resource_search.html.erb +6 -0
  44. data/app/views/avo/private/design.html.erb +2 -2
  45. data/config/routes.rb +0 -3
  46. data/lib/avo/base_card.rb +21 -40
  47. data/lib/avo/base_resource.rb +5 -2
  48. data/lib/avo/concerns/has_editable_controls.rb +34 -0
  49. data/lib/avo/concerns/has_fields.rb +11 -10
  50. data/lib/avo/dashboards/base_dashboard.rb +50 -15
  51. data/lib/avo/dashboards/base_divider.rb +1 -5
  52. data/lib/avo/dashboards/chartkick_card.rb +4 -5
  53. data/lib/avo/fields/base_field.rb +13 -7
  54. data/lib/avo/fields/date_field.rb +1 -3
  55. data/lib/avo/fields/field_extensions/has_include_blank.rb +1 -1
  56. data/lib/avo/fields/field_extensions/visible_in_different_views.rb +1 -12
  57. data/lib/avo/fields/has_base_field.rb +18 -1
  58. data/lib/avo/fields/key_value_field.rb +6 -6
  59. data/lib/avo/hosts/base_host.rb +2 -1
  60. data/lib/avo/hosts/dashboard_card.rb +1 -1
  61. data/lib/avo/hosts/search_scope_host.rb +7 -0
  62. data/lib/avo/licensing/h_q.rb +12 -4
  63. data/lib/avo/licensing/pro_license.rb +1 -0
  64. data/lib/avo/menu/builder.rb +2 -0
  65. data/lib/avo/resources/controls/action.rb +32 -0
  66. data/lib/avo/resources/controls/actions_list.rb +19 -0
  67. data/lib/avo/resources/controls/back_button.rb +13 -0
  68. data/lib/avo/resources/controls/base_control.rb +59 -0
  69. data/lib/avo/resources/controls/delete_button.rb +13 -0
  70. data/lib/avo/resources/controls/detach_button.rb +13 -0
  71. data/lib/avo/resources/controls/edit_button.rb +13 -0
  72. data/lib/avo/resources/controls/execution_context.rb +58 -0
  73. data/lib/avo/resources/controls/items_holder.rb +19 -0
  74. data/lib/avo/resources/controls/link_to.rb +27 -0
  75. data/lib/avo/tab.rb +1 -3
  76. data/lib/avo/version.rb +1 -1
  77. data/lib/generators/avo/templates/action.tt +1 -1
  78. data/lib/generators/avo/templates/cards/partial_card_partial.tt +1 -1
  79. data/lib/generators/avo/templates/initializer/avo.tt +1 -0
  80. data/lib/generators/avo/templates/resource/resource.tt +1 -1
  81. data/lib/generators/avo/templates/resource_tools/partial.tt +1 -1
  82. data/lib/generators/avo/templates/standalone_action.tt +1 -1
  83. data/public/avo-assets/avo.css +592 -124
  84. data/public/avo-assets/avo.js +48 -48
  85. data/public/avo-assets/avo.js.map +3 -3
  86. metadata +17 -10
  87. data/app/components/avo/cards_list_component.html.erb +0 -16
  88. data/app/components/avo/cards_list_component.rb +0 -13
  89. data/lib/avo/concerns/has_cards.rb +0 -88
  90. data/lib/avo/concerns/has_model.rb +0 -11
  91. data/lib/avo/concerns/styles_cards.rb +0 -48
@@ -5,65 +5,151 @@
5
5
  selected_resources: [@resource.model.id],
6
6
  **@resource.stimulus_data_attributes
7
7
  } do %>
8
- <% if @resource.cards.present? %>
9
- <%= content_tag :div, class: "mb-6" do %>
10
- <%= render Avo::CardsListComponent.new parent: @resource %>
11
- <% end %>
12
- <% end %>
13
- <%= render Avo::PanelComponent.new(title: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
8
+ <%= render Avo::PanelComponent.new(name: title, description: @resource.resource_description, display_breadcrumbs: @reflection.blank?, index: 0, data: { panel_id: "main" }) do |c| %>
14
9
  <% c.tools do %>
15
- <% if @reflection.present? && @resource.model.present? %>
16
- <% if can_detach? %>
17
- <%= a_button url: detach_path,
18
- icon: 'detach',
19
- method: :delete,
20
- form_class: 'flex flex-col sm:flex-row sm:inline-flex',
21
- style: :text,
22
- data: {
23
- confirm: "Are you sure you want to detach this #{title}."
24
- } do %>
25
- <%= t('avo.detach_item', item: title).capitalize %>
26
- <% end %>
27
- <% end %>
28
- <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
29
- <% if can_see_the_edit_button? %>
30
- <%= a_link edit_path,
31
- color: :primary,
32
- style: :primary,
33
- icon: 'edit' do %>
34
- <%= t('avo.edit').capitalize %>
10
+ <% if @resource.has_show_controls? %>
11
+ <% @resource.render_show_controls.each do |control| %>
12
+ <% if control.back_button? %>
13
+ <%= a_link back_path,
14
+ style: :text,
15
+ title: control.title,
16
+ data: {
17
+ tippy: control.title ? :tooltip : nil,
18
+ },
19
+ icon: 'arrow-left' do %>
20
+ <%= control.label %>
21
+ <% end %>
22
+ <% elsif control.delete_button? %>
23
+ <% if can_see_the_destroy_button? %>
24
+ <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
25
+ method: :delete,
26
+ local: true,
27
+ style: :text,
28
+ loading: true,
29
+ confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
30
+ color: :red,
31
+ icon: 'trash',
32
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
33
+ title: control.title,
34
+ data: {
35
+ control: :destroy,
36
+ tippy: control.title ? :tooltip : nil,
37
+ 'resource-id': @resource.model.id,
38
+ } do %>
39
+ <%= control.label %>
40
+ <% end %>
41
+ <% end %>
42
+ <% elsif control.actions_list? %>
43
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view, exclude: control.exclude, style: control.style, color: control.color %>
44
+ <% elsif control.edit_button? %>
45
+ <% if @resource.authorization.authorize_action(:edit, raise_exception: false) %>
46
+ <% end %>
47
+ <%= a_link edit_path,
48
+ color: :primary,
49
+ style: :primary,
50
+ title: control.title,
51
+ data: {
52
+ tippy: control.title ? :tooltip : nil,
53
+ },
54
+ icon: 'edit' do %>
55
+ <%= control.label %>
56
+ <% end %>
57
+ <% elsif control.action? %>
58
+ <%= a_link control.path,
59
+ color: control.color,
60
+ style: control.style,
61
+ icon: control.icon,
62
+ title: control.title,
63
+ data: {
64
+ tippy: control.title ? :tooltip : nil,
65
+ 'turbo-frame': 'actions_show',
66
+ 'action': 'click->actions-picker#visitAction',
67
+ } do %>
68
+ <%= control.label %>
69
+ <% end %>
70
+ <% elsif control.link_to? %>
71
+ <%= a_link control.path,
72
+ color: control.color,
73
+ style: control.style,
74
+ icon: control.icon,
75
+ title: control.title,
76
+ target: control.target,
77
+ class: control.class,
78
+ data: {
79
+ **control.data,
80
+ tippy: control.title ? :tooltip : nil,
81
+ } do %>
82
+ <%= control.label %>
83
+ <% end %>
84
+ <% elsif control.detach_button? %>
85
+ <% if @reflection.present? && @resource.model.present? && can_detach? %>
86
+ <%= a_button url: detach_path,
87
+ icon: 'detach',
88
+ method: :delete,
89
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
90
+ style: :text,
91
+ data: {
92
+ confirm: "Are you sure you want to detach this #{title}."
93
+ } do %>
94
+ <%= control.label %>
95
+ <% end %>
96
+ <% end %>
35
97
  <% end %>
36
98
  <% end %>
37
99
  <% else %>
38
- <%= a_link back_path,
39
- style: :text,
40
- icon: 'arrow-left' do %>
41
- <%= t('avo.go_back') %>
42
- <% end %>
43
- <% if can_see_the_destroy_button? %>
44
- <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
45
- method: :delete,
46
- local: true,
100
+ <% if @reflection.present? && @resource.model.present? %>
101
+ <% if can_detach? %>
102
+ <%= a_button url: detach_path,
103
+ icon: 'detach',
104
+ method: :delete,
105
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
106
+ style: :text,
107
+ data: {
108
+ confirm: "Are you sure you want to detach this #{title}."
109
+ } do %>
110
+ <%= t('avo.detach_item', item: title).capitalize %>
111
+ <% end %>
112
+ <% end %>
113
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
114
+ <% if can_see_the_edit_button? %>
115
+ <%= a_link edit_path,
116
+ color: :primary,
117
+ style: :primary,
118
+ icon: 'edit' do %>
119
+ <%= t('avo.edit').capitalize %>
120
+ <% end %>
121
+ <% end %>
122
+ <% else %>
123
+ <%= a_link back_path,
47
124
  style: :text,
48
- loading: true,
49
- confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
50
- color: :red,
51
- icon: 'trash',
52
- form_class: 'flex flex-col sm:flex-row sm:inline-flex',
53
- data: {
54
- control: :destroy,
55
- 'resource-id': @resource.model.id,
56
- } do %>
57
- <%= t('avo.delete').capitalize %>
125
+ icon: 'arrow-left' do %>
126
+ <%= t('avo.go_back') %>
58
127
  <% 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 %>
128
+ <% if can_see_the_destroy_button? %>
129
+ <%= a_button url: helpers.resource_path(model: @resource.model, resource: @resource),
130
+ method: :delete,
131
+ local: true,
132
+ style: :text,
133
+ loading: true,
134
+ confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
135
+ color: :red,
136
+ icon: 'trash',
137
+ form_class: 'flex flex-col sm:flex-row sm:inline-flex',
138
+ data: {
139
+ control: :destroy,
140
+ 'resource-id': @resource.model.id,
141
+ } do %>
142
+ <%= t('avo.delete').capitalize %>
143
+ <% end %>
144
+ <% end %>
145
+ <%= render Avo::ActionsComponent.new actions: @actions, resource: @resource, view: @view %>
146
+ <% if @resource.authorization.authorize_action(:edit, raise_exception: false) %>
147
+ <%= a_link edit_path,
148
+ color: :primary,
149
+ style: :primary,
150
+ icon: 'edit' do %>
151
+ <%= t('avo.edit').capitalize %>
152
+ <% end %>
67
153
  <% end %>
68
154
  <% end %>
69
155
  <% end %>
@@ -17,6 +17,7 @@ class Avo::Views::ResourceShowComponent < Avo::ResourceComponent
17
17
  def title
18
18
  if @reflection.present?
19
19
  return field.name if has_one_field?
20
+
20
21
  reflection_resource.name
21
22
  else
22
23
  @resource.default_panel_name
@@ -118,7 +118,7 @@ module Avo
118
118
  def set_resource
119
119
  raise ActionController::RoutingError.new "No route matches" if resource.nil?
120
120
 
121
- @resource = resource
121
+ @resource = resource.hydrate(params: params)
122
122
  end
123
123
 
124
124
  def set_related_resource
@@ -126,7 +126,6 @@ module Avo
126
126
  end
127
127
 
128
128
  def set_model
129
- puts ["controller_name->", controller_name].inspect
130
129
  @model = eager_load_files(@resource, @resource.class.find_scope).find params[:id]
131
130
  end
132
131
 
@@ -157,7 +156,7 @@ module Avo
157
156
  end
158
157
 
159
158
  def hydrate_resource
160
- @resource.hydrate(view: action_name.to_sym, user: _current_user, model: @model, params: params)
159
+ @resource.hydrate(view: action_name.to_sym, user: _current_user)
161
160
  end
162
161
 
163
162
  def hydrate_related_resource
@@ -208,6 +207,11 @@ module Avo
208
207
  end
209
208
 
210
209
  def related_resource
210
+ # Find the field from the parent resource
211
+ field = @resource.get_field params[:related_name]
212
+
213
+ return field.use_resource if field&.use_resource.present?
214
+
211
215
  reflection = @model._reflections[params[:related_name]]
212
216
 
213
217
  reflected_model = reflection.klass
@@ -6,8 +6,8 @@ module Avo
6
6
  before_action :set_related_resource_name
7
7
  before_action :set_related_resource, only: [:show, :index, :new, :create, :destroy, :order]
8
8
  before_action :set_reflection_field
9
- before_action :set_related_model, only: [:show, :order]
10
9
  before_action :hydrate_related_resource, only: [:show, :index, :create, :destroy, :order]
10
+ before_action :set_related_model, only: [:show, :order]
11
11
  before_action :set_reflection
12
12
  before_action :set_attachment_class, only: [:show, :index, :new, :create, :destroy, :order]
13
13
  before_action :set_attachment_resource, only: [:show, :index, :new, :create, :destroy, :order]
@@ -102,7 +102,7 @@ module Avo
102
102
  end
103
103
 
104
104
  def set_attachment_resource
105
- @attachment_resource = App.get_resource_by_model_name @attachment_class
105
+ @attachment_resource = @field.use_resource || (App.get_resource_by_model_name @attachment_class)
106
106
  end
107
107
 
108
108
  def set_attachment_model
@@ -7,7 +7,6 @@ module Avo
7
7
  before_action :hydrate_resource
8
8
  before_action :set_applied_filters, only: :index
9
9
  before_action :set_model, only: [:show, :edit, :destroy, :update, :order]
10
- before_action :hydrate_resource
11
10
  before_action :set_model_to_fill
12
11
  before_action :set_edit_title_and_breadcrumbs, only: [:edit, :update]
13
12
  before_action :fill_model, only: [:create, :update]
@@ -101,7 +100,7 @@ module Avo
101
100
 
102
101
  def new
103
102
  @model = @resource.model_class.new
104
- @resource = @resource.hydrate(model: @model, view: :new)
103
+ @resource = @resource.hydrate(model: @model, view: :new, user: _current_user)
105
104
 
106
105
  set_actions
107
106
 
@@ -116,7 +115,7 @@ module Avo
116
115
  add_breadcrumb via_resource.model_title, resource_path(model: via_model, resource: via_resource)
117
116
  end
118
117
 
119
- add_breadcrumb @resource.plural_name.humanize
118
+ add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
120
119
  add_breadcrumb t("avo.new").humanize
121
120
  end
122
121
 
@@ -130,21 +129,14 @@ module Avo
130
129
  @reflection = @model._reflections[params[:via_relation]]
131
130
  # Figure out what kind of association does the record have with the parent record
132
131
 
133
- # belongs_to
134
- # has_many
132
+ # Fills in the required infor for belongs_to and has_many
135
133
  # Get the foreign key and set it to the id we received in the params
136
134
  if @reflection.is_a?(ActiveRecord::Reflection::BelongsToReflection) || @reflection.is_a?(ActiveRecord::Reflection::HasManyReflection)
137
- foreign_key = @reflection.foreign_key
138
- @model.send("#{foreign_key}=", params[:via_resource_id])
135
+ @model.send("#{@reflection.foreign_key}=", params[:via_resource_id])
139
136
  @model.save
140
137
  end
141
138
 
142
- # has_one
143
- # has_one_through
144
-
145
- # has_many_through
146
- # has_and_belongs_to_many
147
- # polymorphic
139
+ # For when working with has_one, has_one_through, has_many_through, has_and_belongs_to_many, polymorphic
148
140
  if @reflection.is_a? ActiveRecord::Reflection::ThroughReflection
149
141
  # find the record
150
142
  via_resource = ::Avo::App.get_resource_by_model_name params[:via_relation_class]
@@ -158,6 +150,9 @@ module Avo
158
150
  if saved
159
151
  format.html { redirect_to after_create_path, notice: "#{@resource.name} #{t("avo.was_successfully_created")}." }
160
152
  else
153
+ add_breadcrumb @resource.plural_name.humanize, resources_path(resource: @resource)
154
+ add_breadcrumb t("avo.new").humanize
155
+ set_actions
161
156
  flash.now[:error] = t "avo.you_missed_something_check_form"
162
157
  format.html { render :new, status: :unprocessable_entity }
163
158
  end
@@ -171,12 +166,13 @@ module Avo
171
166
  def update
172
167
  # model gets instantiated and filled in the fill_model method
173
168
  saved = save_model
174
- @resource = @resource.hydrate(model: @model, view: :edit)
169
+ @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
175
170
 
176
171
  respond_to do |format|
177
172
  if saved
178
173
  format.html { redirect_to after_update_path, notice: "#{@resource.name} #{t("avo.was_successfully_updated")}." }
179
174
  else
175
+ set_actions
180
176
  flash.now[:error] = t "avo.you_missed_something_check_form"
181
177
  format.html { render :edit, status: :unprocessable_entity }
182
178
  end
@@ -210,9 +206,6 @@ module Avo
210
206
  end
211
207
  end
212
208
 
213
- def cards
214
- end
215
-
216
209
  private
217
210
 
218
211
  def save_model
@@ -380,7 +373,7 @@ module Avo
380
373
  end
381
374
 
382
375
  def set_edit_title_and_breadcrumbs
383
- @resource = @resource.hydrate(model: @model, view: :edit)
376
+ @resource = @resource.hydrate(model: @model, view: :edit, user: _current_user)
384
377
  @page_title = @resource.default_panel_name.to_s
385
378
 
386
379
  last_crumb_args = {}
@@ -2,12 +2,7 @@ require_dependency "avo/application_controller"
2
2
 
3
3
  module Avo
4
4
  class CardsController < ApplicationController
5
- before_action :set_dashboard
6
- before_action :set_resource_name
7
- before_action :set_resource, if: -> { @dashboard.blank? }
8
- before_action :set_model, only: :show, if: -> { @resource.present? }
9
- before_action :hydrate_resource, if: -> { @resource.present? }
10
- before_action :set_parent, only: :show
5
+ before_action :set_dashboard, only: :show
11
6
  before_action :set_card, only: :show
12
7
 
13
8
  def show
@@ -15,23 +10,15 @@ module Avo
15
10
 
16
11
  private
17
12
 
18
- def set_parent
19
- @parent = @dashboard || @resource
20
- end
21
-
22
13
  def set_dashboard
23
- return if params[:dashboard_id].blank?
24
-
25
- @dashboard_class = Avo::App.get_dashboard_by_id params[:dashboard_id]
26
-
27
- raise ActionController::RoutingError.new("Not Found") if @dashboard_class.nil? || @dashboard_class.is_hidden?
14
+ @dashboard = Avo::App.get_dashboard_by_id params[:dashboard_id]
28
15
 
29
- @dashboard = @dashboard_class.new
16
+ raise ActionController::RoutingError.new("Not Found") if @dashboard.nil? || @dashboard.is_hidden?
30
17
  end
31
18
 
32
19
  def set_card
33
- @card = @parent.item_at_index(params[:index].to_i).tap do |card|
34
- card.hydrate(parent: @parent, params: params)
20
+ @card = @dashboard.item_at_index(params[:index].to_i).tap do |card|
21
+ card.hydrate(dashboard: @dashboard, params: params)
35
22
  end
36
23
  end
37
24
  end
@@ -10,11 +10,9 @@ module Avo
10
10
  private
11
11
 
12
12
  def set_dashboard
13
- @dashboard_class = Avo::App.get_dashboard_by_id params[:id]
13
+ @dashboard = Avo::App.get_dashboard_by_id params[:id]
14
14
 
15
- raise ActionController::RoutingError.new("Not Found") if @dashboard_class.nil? || @dashboard_class.is_hidden?
16
-
17
- @dashboard = @dashboard_class.new.hydrate(params: params) if @dashboard_class.present?
15
+ raise ActionController::RoutingError.new("Not Found") if @dashboard.nil? || @dashboard.is_hidden?
18
16
  end
19
17
  end
20
18
  end
@@ -44,10 +44,13 @@ module Avo
44
44
  end
45
45
 
46
46
  def search_resource(resource)
47
- query = resource.search_query.call(params: params).limit(8)
47
+ query = Avo::Hosts::SearchScopeHost.new(
48
+ block: resource.search_query,
49
+ params: params,
50
+ scope: resource.class.scope
51
+ ).handle
48
52
 
49
- # Figure out if this is a belongs_to search
50
- query = apply_attach_scope query
53
+ query = apply_scope(query) if should_apply_any_scope?
51
54
 
52
55
  results = apply_search_metadata(query, resource)
53
56
 
@@ -67,22 +70,19 @@ module Avo
67
70
  [resource.name.pluralize.downcase, result_object]
68
71
  end
69
72
 
70
- # Figure out if it's a belongs to search and if it has the attach_scope block present.
71
- # If so, set the parent for those edit view and the parent with the grandparent for the new view.
72
- def apply_attach_scope(query)
73
- return query if params[:via_reflection_class].blank?
74
-
75
- # Fetch the field
76
- field = belongs_to_field
77
-
78
- # No need to modify the query if there's no attach_scope present.
79
- return query if field.attach_scope.blank?
80
-
81
- # Try to fetch the parent.
82
- if params[:via_reflection_id].present?
83
- parent = params[:via_reflection_class].safe_constantize.find params[:via_reflection_id]
73
+ # When searching in a `has_many` association and will scope out the records against the parent record.
74
+ # This is also used when looking for `belongs_to` associations, and this method applies the parents `attach_scope` if present.
75
+ def apply_scope(query)
76
+ if should_apply_has_many_scope?
77
+ apply_has_many_scope
78
+ elsif should_apply_attach_scope?
79
+ apply_attach_scope(query, parent)
84
80
  end
81
+ end
85
82
 
83
+ # Parent passed as argument to be used as a variable instead of the method "def parent"
84
+ # Otherwise parent = params...safe_constantize... will try to call method "def parent="
85
+ def apply_attach_scope(query, parent)
86
86
  # If the parent is nil it probably means that someone's creating the record so it's not attached yet.
87
87
  # In these scenarios, try to find the grandparent for the new views where the parent is nil
88
88
  # and initialize the parent record with the grandparent attached so the user has the required information
@@ -94,8 +94,13 @@ module Avo
94
94
  )
95
95
  end
96
96
 
97
- # Add to the query
98
- Avo::Hosts::AssociationScopeHost.new(block: belongs_to_field.attach_scope, query: query, parent: parent).handle
97
+ Avo::Hosts::AssociationScopeHost.new(block: attach_scope, query: query, parent: parent).handle
98
+ end
99
+
100
+ def apply_has_many_scope
101
+ scope = parent.send(params[:via_association_id])
102
+
103
+ Avo::Hosts::SearchScopeHost.new(block: @resource.search_query, params: params, scope: scope).handle
99
104
  end
100
105
 
101
106
  def apply_search_metadata(models, avo_resource)
@@ -118,9 +123,41 @@ module Avo
118
123
  end
119
124
  end
120
125
 
121
- def belongs_to_field
126
+ private
127
+
128
+ def should_apply_has_many_scope?
129
+ params[:via_association] == "has_many" && @resource.search_query.present?
130
+ end
131
+
132
+ def should_apply_attach_scope?
133
+ params[:via_association] == "belongs_to" && attach_scope.present?
134
+ end
135
+
136
+ def should_apply_any_scope?
137
+ should_apply_has_many_scope? || should_apply_attach_scope?
138
+ end
139
+
140
+ def attach_scope
141
+ @attach_scope ||= field&.attach_scope
142
+ end
143
+
144
+ def field
145
+ @field ||= fetch_field
146
+ end
147
+
148
+ def parent
149
+ @parent ||= fetch_parent
150
+ end
151
+
152
+ def fetch_field
122
153
  fields = ::Avo::App.get_resource_by_model_name(params[:via_reflection_class]).get_field_definitions
123
154
  fields.find { |f| f.id.to_s == params[:via_association_id] }
124
155
  end
156
+
157
+ def fetch_parent
158
+ return unless params[:via_reflection_id].present?
159
+
160
+ params[:via_reflection_class].safe_constantize.find params[:via_reflection_id]
161
+ end
125
162
  end
126
163
  end
@@ -22,8 +22,8 @@ export default class extends Controller {
22
22
  }
23
23
 
24
24
  get initiallyCollapsed() {
25
- if (this.defaultState === 'collapsed') {
26
- return this.userState === 'collapsed'
25
+ if (!this.userState) {
26
+ return this.defaultState === 'collapsed'
27
27
  }
28
28
 
29
29
  return this.userState === 'collapsed'
@@ -48,6 +48,10 @@ export default class extends Controller {
48
48
  return this.dataset.viaAssociation === 'belongs_to'
49
49
  }
50
50
 
51
+ get isHasManySearch() {
52
+ return this.dataset.viaAssociation === 'has_many'
53
+ }
54
+
51
55
  get isGlobalSearch() {
52
56
  return this.dataset.searchResource === 'global'
53
57
  }
@@ -83,27 +87,50 @@ export default class extends Controller {
83
87
  params.global = true
84
88
  }
85
89
 
86
- if (this.isBelongsToSearch) {
87
- params = {
88
- ...params,
89
- // eslint-disable-next-line camelcase
90
- via_association_id: this.dataset.viaAssociationId,
91
- // eslint-disable-next-line camelcase
92
- via_reflection_class: this.dataset.viaReflectionClass,
93
- // eslint-disable-next-line camelcase
94
- via_reflection_id: this.dataset.viaReflectionId,
95
- // eslint-disable-next-line camelcase
96
- via_parent_resource_id: this.dataset.viaParentResourceId,
97
- // eslint-disable-next-line camelcase
98
- via_parent_resource_class: this.dataset.viaParentResourceClass,
99
- // eslint-disable-next-line camelcase
100
- via_relation: this.dataset.viaRelation,
90
+ if (this.isBelongsToSearch || this.isHasManySearch) {
91
+ params = this.addAssociationParams(params)
92
+ params = this.addReflectionParams(params)
93
+
94
+ if (this.isBelongsToSearch) {
95
+ params = {
96
+ ...params,
97
+ // eslint-disable-next-line camelcase
98
+ via_parent_resource_id: this.dataset.viaParentResourceId,
99
+ // eslint-disable-next-line camelcase
100
+ via_parent_resource_class: this.dataset.viaParentResourceClass,
101
+ // eslint-disable-next-line camelcase
102
+ via_relation: this.dataset.viaRelation,
103
+ }
101
104
  }
102
105
  }
103
106
 
104
107
  return params
105
108
  }
106
109
 
110
+ addAssociationParams(params) {
111
+ params = {
112
+ ...params,
113
+ // eslint-disable-next-line camelcase
114
+ via_association: this.dataset.viaAssociation,
115
+ // eslint-disable-next-line camelcase
116
+ via_association_id: this.dataset.viaAssociationId,
117
+ }
118
+
119
+ return params
120
+ }
121
+
122
+ addReflectionParams(params) {
123
+ params = {
124
+ ...params,
125
+ // eslint-disable-next-line camelcase
126
+ via_reflection_class: this.dataset.viaReflectionClass,
127
+ // eslint-disable-next-line camelcase
128
+ via_reflection_id: this.dataset.viaReflectionId,
129
+ }
130
+
131
+ return params
132
+ }
133
+
107
134
  handleOnSelect({ item }) {
108
135
  if (this.isBelongsToSearch) {
109
136
  this.updateFieldAttribute(this.hiddenIdTarget, 'value', item._id)
@@ -5,9 +5,9 @@ import ActionsPickerController from './controllers/actions_picker_controller'
5
5
  import AttachmentsController from './controllers/attachments_controller'
6
6
  import BelongsToFieldController from './controllers/fields/belongs_to_field_controller'
7
7
  import BooleanFilterController from './controllers/boolean_filter_controller'
8
- import CardController from './controllers/card_controller'
9
8
  import CodeFieldController from './controllers/fields/code_field_controller'
10
9
  import CopyToClipboardController from './controllers/copy_to_clipboard_controller'
10
+ import DashboardCardController from './controllers/dashboard_card_controller'
11
11
  import DateFieldController from './controllers/fields/date_field_controller'
12
12
  import FilterController from './controllers/filter_controller'
13
13
  import HiddenInputController from './controllers/hidden_input_controller'
@@ -38,8 +38,8 @@ application.register('action', ActionController)
38
38
  application.register('actions-picker', ActionsPickerController)
39
39
  application.register('attachments', AttachmentsController)
40
40
  application.register('boolean-filter', BooleanFilterController)
41
- application.register('card', CardController)
42
41
  application.register('copy-to-clipboard', CopyToClipboardController)
42
+ application.register('dashboard-card', DashboardCardController)
43
43
  application.register('filter', FilterController)
44
44
  application.register('hidden-input', HiddenInputController)
45
45
  application.register('item-select-all', ItemSelectAllController)
@@ -13,6 +13,7 @@
13
13
  <div class="flex-1 flex items-center justify-center px-0 lg:px-8 text-lg mt-8 mb-12">
14
14
  <% if @field.searchable %>
15
15
  <%= render Avo::Fields::BelongsToField::AutocompleteComponent.new form: form,
16
+ classes: input_classes("w-full"),
16
17
  field: @field,
17
18
  model_key: @field.target_resource&.model_key,
18
19
  foreign_key: 'related_id',
@@ -1,5 +1,5 @@
1
- <div class="flex items-end text-gray-800">
1
+ <div class="flex mt-4 items-end">
2
2
  <span class="text-3xl"><%= @card.prefix %></span>
3
- <span class="text-5xl font-semibold"><%= @card.result_data %></span>
3
+ <span class="text-5xl"><%= @card.result_data %></span>
4
4
  <span class="text-3xl"><%= @card.suffix %></span>
5
5
  </div>