avo 2.1.0 → 2.1.2.pre2

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 (40) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile.lock +1 -1
  3. data/app/assets/builds/avo.css +8810 -0
  4. data/app/assets/builds/avo.js +423 -0
  5. data/app/assets/builds/avo.js.map +7 -0
  6. data/app/assets/stylesheets/avo.css +1 -1
  7. data/app/components/avo/alert_component.html.erb +1 -1
  8. data/app/components/avo/alert_component.rb +1 -1
  9. data/app/components/avo/card_component.html.erb +7 -2
  10. data/app/components/avo/filters_component.html.erb +19 -20
  11. data/app/components/avo/panel_component.html.erb +5 -3
  12. data/app/components/avo/sidebar_component.html.erb +2 -0
  13. data/app/components/avo/sidebar_profile_component.html.erb +14 -10
  14. data/app/components/avo/views/resource_show_component.html.erb +10 -0
  15. data/app/components/avo/views/resource_show_component.rb +5 -0
  16. data/app/controllers/avo/base_controller.rb +30 -10
  17. data/app/controllers/avo/dashboards_controller.rb +1 -5
  18. data/app/controllers/avo/search_controller.rb +1 -1
  19. data/app/helpers/avo/url_helpers.rb +6 -2
  20. data/app/views/avo/dashboards/show.html.erb +4 -1
  21. data/app/views/avo/partials/_sidebar_extra.html.erb +0 -0
  22. data/lib/avo/app.rb +7 -3
  23. data/lib/avo/base_action.rb +1 -0
  24. data/lib/avo/base_card.rb +175 -0
  25. data/lib/avo/base_resource.rb +12 -2
  26. data/lib/avo/dashboards/base_dashboard.rb +20 -2
  27. data/lib/avo/dashboards/base_divider.rb +3 -1
  28. data/lib/avo/dashboards/dashboard_card.rb +6 -0
  29. data/lib/avo/dashboards/partial_card.rb +1 -1
  30. data/lib/avo/dynamic_router.rb +1 -1
  31. data/lib/avo/engine.rb +7 -16
  32. data/lib/avo/fields/base_field.rb +2 -2
  33. data/lib/avo/fields/date_field.rb +1 -1
  34. data/lib/avo/fields_collector.rb +7 -2
  35. data/lib/avo/hosts/dashboard_card.rb +1 -0
  36. data/lib/avo/reloader.rb +51 -0
  37. data/lib/avo/version.rb +1 -1
  38. data/public/avo-assets/avo.css +18 -13
  39. metadata +11 -5
  40. data/lib/avo/dashboards/base_card.rb +0 -151
@@ -74,7 +74,7 @@ body {
74
74
  }
75
75
 
76
76
  .turbo-progress-bar {
77
- @apply bg-green-500;
77
+ @apply bg-blue-400;
78
78
  }
79
79
 
80
80
  body.os-mac .mac\:hidden {
@@ -10,7 +10,7 @@
10
10
  </div>
11
11
  <div class="ml-3 w-0 flex-1 pt-0.5">
12
12
  <p class="text-sm leading-5 font-semibold">
13
- <%= message %>
13
+ <%== message %>
14
14
  </p>
15
15
  </div>
16
16
  <div class="ml-4 flex-shrink-0 flex">
@@ -18,7 +18,7 @@ class Avo::AlertComponent < ViewComponent::Base
18
18
  end
19
19
 
20
20
  def classes
21
- result = "max-w-sm w-full shadow-lg rounded px-4 py-3 rounded relative border text-white pointer-events-auto"
21
+ result = "max-w-lg w-full shadow-lg rounded px-4 py-3 rounded relative border text-white pointer-events-auto"
22
22
 
23
23
  result += if is_error?
24
24
  " bg-red-400 border-red-700"
@@ -1,4 +1,9 @@
1
- <div class="relative flex-1 flex flex-col justify-between h-full" data-controller="dashboard-card" data-dashboard-card-target="card" data-refresh-every="<%= @card.refresh_every %>" data-card-id="<%= @card.id %>">
1
+ <div class="relative flex-1 flex flex-col justify-between h-full"
2
+ data-controller="dashboard-card"
3
+ data-dashboard-card-target="card"
4
+ data-refresh-every="<%= @card.refresh_every %>"
5
+ data-card-id="<%= @card.id %>"
6
+ data-card-index="<%= @card.index %>">
2
7
  <% if @card.class.display_header %>
3
8
  <div class="px-4 pt-4">
4
9
  <div class="flex justify-between items-center min-h-6">
@@ -7,7 +12,7 @@
7
12
  </div>
8
13
  <div data-controller="select">
9
14
  <% if @card.type == :metric && @card.parsed_ranges.present? %>
10
- <%= select_tag "#{@card.id}_range", options_for_select(@card.parsed_ranges, @card.range),
15
+ <%= select_tag "#{@card.id}_#{@card.index}_range", options_for_select(@card.parsed_ranges, @card.range),
11
16
  class: 'appearance-none inline-flex bg-blue-gray-100 disabled:bg-blue-gray-300 disabled:cursor-not-allowed focus:bg-white text-sm text-blue-gray-700 disabled:text-blue-gray-700 leading-none rounded-md py-px px-2 leading-tight border outline-none outline w-24',
12
17
  data: {
13
18
  target: 'select',
@@ -7,31 +7,30 @@
7
7
  title: t('avo.click_to_reveal_filters'),
8
8
  'data-button': 'resource-filters',
9
9
  'data-action': 'click->toggle-panel#togglePanel',
10
- 'data-tippy': 'tooltip' do
11
- %>
12
- <%= t 'avo.filters' %>
13
- <% if params[:filters].present? %>
14
- <span class="ml-1">(<%=JSON.parse(Base64.decode64(params[:filters])).count%> applied)</span>
10
+ 'data-tippy': 'tooltip' do %>
11
+ <%= t 'avo.filters' %>
12
+ <% if params[:filters].present? %>
13
+ <span class="ml-1">(<%=JSON.parse(Base64.decode64(params[:filters])).count%> applied)</span>
14
+ <% end %>
15
15
  <% end %>
16
- <% end %>
17
- <div
16
+ <div
18
17
  class="absolute block inset-auto sm:right-0 top-full bg-white min-w-[300px] mt-2 z-20 shadow-modal rounded hidden divide-y divide-gray-300"
19
18
  data-toggle-panel-target="panel"
20
19
  >
21
- <% @filters.each do |filter| %>
22
- <%= render partial: filter.class.template, locals: {filter: filter} %>
23
- <% end %>
24
- <div class="p-4 border-gray-300 border-t">
25
- <% if params[:filters].present? %>
26
- <%= a_link helpers.resources_path(resource: @resource, filters: nil, keep_query_params: true), color: :gray, size: :sm, class: 'w-full justify-center' do %>
27
- <%= t('avo.reset_filters') %>
28
- <% end %>
29
- <% else %>
30
- <%= a_button class: 'w-full justify-center', disabled: true do %>
31
- <%= t('avo.reset_filters') %>
32
- <% end %>
20
+ <% @filters.each do |filter| %>
21
+ <%= render partial: filter.class.template, locals: {filter: filter} %>
33
22
  <% end %>
23
+ <div class="p-4 border-gray-300 border-t">
24
+ <% if params[:filters].present? %>
25
+ <%= a_link helpers.resources_path(resource: @resource, filters: nil, keep_query_params: true), color: :gray, size: :sm, class: 'w-full justify-center' do %>
26
+ <%= t('avo.reset_filters') %>
27
+ <% end %>
28
+ <% else %>
29
+ <%= a_button class: 'w-full justify-center', disabled: true do %>
30
+ <%= t('avo.reset_filters') %>
31
+ <% end %>
32
+ <% end %>
33
+ </div>
34
34
  </div>
35
35
  </div>
36
36
  </div>
37
- </div>
@@ -19,9 +19,11 @@
19
19
  <% end %>
20
20
  </div>
21
21
 
22
- <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">
23
- <%= tools %>
24
- </div>
22
+ <% if tools.present? %>
23
+ <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
+ <%= tools %>
25
+ </div>
26
+ <% end %>
25
27
  </div>
26
28
  <% end %>
27
29
 
@@ -43,6 +43,8 @@
43
43
  </div>
44
44
  </div>
45
45
  <% end %>
46
+
47
+ <%= render partial: "/avo/partials/sidebar_extra" %>
46
48
  </div>
47
49
  </div>
48
50
  <%= helpers.render_license_warnings %>
@@ -1,19 +1,23 @@
1
1
  <div class="text-black border-t border-gray-200 p-4 flex">
2
2
  <div class="flex-1 flex space-x-4">
3
- <div class="relative aspect-square w-10 h-10 overflow-hidden rounded">
4
- <%= image_tag avatar, class: "object-cover min-w-full min-h-full h-full" %>
5
- </div>
3
+ <% if avatar.present? %>
4
+ <div class="relative aspect-square w-10 h-10 overflow-hidden rounded">
5
+ <%= image_tag avatar, class: "object-cover min-w-full min-h-full h-full" %>
6
+ </div>
7
+ <% end %>
6
8
  <div class="flex flex-col">
7
9
  <div class="font-medium">
8
10
  <%= name %>
9
11
  </div>
10
- <div class="text-xs text-gray-500 uppercase">
11
- <%= title %>
12
- </div>
12
+ <% if title.present? %>
13
+ <div class="text-xs text-gray-500 uppercase">
14
+ <%= title %>
15
+ </div>
16
+ <% end %>
13
17
  </div>
14
18
  </div>
15
- <div class="relative" data-controller="toggle-panel">
16
- <% if can_destroy_user? %>
19
+ <% if can_destroy_user? %>
20
+ <div class="relative" data-controller="toggle-panel">
17
21
  <a class="flex items-center h-full cursor-pointer" data-control="profile-dots" data-action="click->toggle-panel#togglePanel">
18
22
  <%= helpers.svg 'three-dots', class: 'h-4' %>
19
23
  </a>
@@ -31,6 +35,6 @@
31
35
  <%= helpers.svg 'logout', class: 'h-4 mr-1' %> <%= t('avo.sign_out') %>
32
36
  <% end %>
33
37
  </div>
34
- <% end %>
35
- </div>
38
+ </div>
39
+ <% end %>
36
40
  </div>
@@ -84,4 +84,14 @@
84
84
  <% end %>
85
85
  <% end %>
86
86
  <% end %>
87
+
88
+ <% if should_display_invalid_fields_errors? %>
89
+ <turbo-stream action="append" target="alerts">
90
+ <template>
91
+ <% @resource.invalid_fields.each do |error| %>
92
+ <%= render Avo::AlertComponent.new :error, error[:message] %>
93
+ <% end %>
94
+ </template>
95
+ </turbo-stream>
96
+ <% end %>
87
97
  </div>
@@ -90,4 +90,9 @@ class Avo::Views::ResourceShowComponent < Avo::ResourceComponent
90
90
  end
91
91
  end
92
92
  end
93
+
94
+ # In development and test environments we shoudl show the invalid field errors
95
+ def should_display_invalid_fields_errors?
96
+ (Rails.env.development? || Rails.env.test?) && @resource.invalid_fields.present?
97
+ end
93
98
  end
@@ -44,7 +44,7 @@ module Avo
44
44
  unless @index_params[:sort_by].eql? :created_at
45
45
  @query = @query.unscope(:order)
46
46
  end
47
- @query = @query.order("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}")
47
+ @query = @query.order(Arel.sql("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}"))
48
48
  end
49
49
 
50
50
  # Apply filters
@@ -128,14 +128,7 @@ module Avo
128
128
 
129
129
  respond_to do |format|
130
130
  if saved
131
- redirect_path = resource_path(model: @model, resource: @resource)
132
-
133
- if params[:via_relation_class].present? && params[:via_resource_id].present?
134
- parent_resource = ::Avo::App.get_resource_by_model_name params[:via_relation_class].safe_constantize
135
- redirect_path = resource_path(model: params[:via_relation_class].safe_constantize, resource: parent_resource, resource_id: params[:via_resource_id])
136
- end
137
-
138
- format.html { redirect_to redirect_path, notice: "#{@model.class.name} #{t("avo.was_successfully_created")}." }
131
+ format.html { redirect_to after_create_path, notice: "#{@model.class.name} #{t("avo.was_successfully_created")}." }
139
132
  else
140
133
  flash.now[:error] = t "avo.you_missed_something_check_form"
141
134
  format.html { render :new, status: :unprocessable_entity }
@@ -153,7 +146,7 @@ module Avo
153
146
 
154
147
  respond_to do |format|
155
148
  if saved
156
- format.html { redirect_to params[:referrer] || resource_path(model: @model, resource: @resource), notice: "#{@model.class.name} #{t("avo.was_successfully_updated")}." }
149
+ format.html { redirect_to after_update_path, notice: "#{@model.class.name} #{t("avo.was_successfully_updated")}." }
157
150
  else
158
151
  flash.now[:error] = t "avo.you_missed_something_check_form"
159
152
  format.html { render :edit, status: :unprocessable_entity }
@@ -357,5 +350,32 @@ module Avo
357
350
  add_breadcrumb @resource.model_title, resource_path(model: @resource.model, resource: @resource)
358
351
  add_breadcrumb t("avo.edit").humanize
359
352
  end
353
+
354
+ def after_create_path
355
+ # If this is an associated record return to the association show page
356
+ if params[:via_relation_class].present? && params[:via_resource_id].present?
357
+ parent_resource = ::Avo::App.get_resource_by_model_name params[:via_relation_class].safe_constantize
358
+
359
+ return resource_path(model: params[:via_relation_class].safe_constantize, resource: parent_resource, resource_id: params[:via_resource_id])
360
+ end
361
+
362
+ redirect_path_from_resource_option || resource_path(model: @model, resource: @resource)
363
+ end
364
+
365
+ def after_update_path
366
+ return params[:referrer] if params[:referrer].present?
367
+
368
+ redirect_path_from_resource_option || resource_path(model: @model, resource: @resource)
369
+ end
370
+
371
+ def redirect_path_from_resource_option
372
+ return nil if @resource.class.after_update_path.blank?
373
+
374
+ if @resource.class.after_create_path == :index
375
+ resources_path(resource: @resource)
376
+ else
377
+ resource_path(model: @model, resource: @resource)
378
+ end
379
+ end
360
380
  end
361
381
  end
@@ -8,11 +8,7 @@ module Avo
8
8
  end
9
9
 
10
10
  def card
11
- @card = @dashboard.items.find do |item|
12
- next unless item.is_card?
13
-
14
- item.id.to_s == params[:card_id]
15
- end.tap do |card|
11
+ @card = @dashboard.item_at_index(params[:index].to_i).tap do |card|
16
12
  card.hydrate(dashboard: @dashboard, params: params)
17
13
  end
18
14
  end
@@ -11,7 +11,7 @@ module Avo
11
11
  raise ActionController::BadRequest.new("This feature requires the pro license https://avohq.io/purchase/pro") if App.license.lacks_with_trial(:global_search)
12
12
 
13
13
  resources = Avo::App.resources.reject do |resource|
14
- resource.class.search_hide_from_global_search
14
+ resource.class.hide_from_global_search
15
15
  end
16
16
 
17
17
  render json: search_resources(resources)
@@ -12,8 +12,12 @@ module Avo
12
12
  end
13
13
  end
14
14
 
15
- # This entry uses `route_key` instead of `model_key` because it's rails that needs `fish_index` to build the correct path
16
- avo.send :"resources_#{resource.route_key}_path", **existing_params, **args
15
+ # Create the `route_key` from the model key so the namespaced models get the proper path (SomeModule::Post -> some_module_post).
16
+ # Add the `_index` suffix for the uncountable models so they get the correct path (`fish_index`)
17
+ route_key = resource.model_key
18
+ route_key << "_index" if resource.model_name.singular == resource.model_name.plural
19
+
20
+ avo.send :"resources_#{route_key}_path", **existing_params, **args
17
21
  end
18
22
 
19
23
  def resource_path(
@@ -8,7 +8,10 @@
8
8
  <%= render Avo::Dashboards::DividerComponent.new(divider: item) %>
9
9
  <% elsif item.is_card? %>
10
10
  <%= content_tag(:div, class: "relative bg-white rounded shadow-panel space-y-2 #{item.card_classes} overflow-hidden") do %>
11
- <turbo-frame id="<%= item.turbo_frame %>" src="<%= item.frame_url(enforced_range: @range) %>">
11
+ <turbo-frame id="<%= item.turbo_frame %>"
12
+ src="<%= item.frame_url(enforced_range: @range, params: params) %>"
13
+ data-card-index="<%= item.index %>"
14
+ >
12
15
  <%= render(Avo::LoadingComponent.new(title: item.label)) %>
13
16
  </turbo-frame>
14
17
  <% end %>
File without changes
data/lib/avo/app.rb CHANGED
@@ -79,12 +79,16 @@ module Avo
79
79
  def init_resources
80
80
  self.resources = BaseResource.descendants
81
81
  .select do |resource|
82
+ # Remove the BaseResource. We only need the descendants
82
83
  resource != BaseResource
83
84
  end
85
+ .uniq do |klass|
86
+ # On invalid resource configuration the resource classes get duplicated in `ObjectSpace`
87
+ # We need to de-duplicate them
88
+ klass.name
89
+ end
84
90
  .map do |resource|
85
- if resource.is_a? Class
86
- resource.new
87
- end
91
+ resource.new if resource.is_a? Class
88
92
  end
89
93
  end
90
94
 
@@ -13,6 +13,7 @@ module Avo
13
13
  class_attribute :user
14
14
  class_attribute :resource
15
15
  class_attribute :fields
16
+ class_attribute :invalid_fields
16
17
  class_attribute :standalone, default: false
17
18
  class_attribute :visible
18
19
  class_attribute :may_download_file, default: false
@@ -0,0 +1,175 @@
1
+ module Avo
2
+ class BaseCard
3
+ class_attribute :id
4
+ class_attribute :label
5
+ class_attribute :description
6
+ class_attribute :cols, default: 1
7
+ class_attribute :rows, default: 1
8
+ class_attribute :initial_range
9
+ class_attribute :ranges, default: []
10
+ class_attribute :refresh_every
11
+ class_attribute :display_header, default: true
12
+ # private
13
+ class_attribute :result_data
14
+ class_attribute :query_block
15
+
16
+ attr_accessor :dashboard
17
+ attr_accessor :options
18
+ attr_accessor :index
19
+ attr_accessor :params
20
+
21
+ delegate :context, to: ::Avo::App
22
+
23
+ class << self
24
+ def query(&block)
25
+ self.query_block = block
26
+ end
27
+ end
28
+
29
+ def initialize(dashboard:, options: {}, index: 0, cols: nil, rows: nil, label: nil, description: nil, refresh_every: nil)
30
+ @dashboard = dashboard
31
+ @options = options
32
+ @index = index
33
+ @cols = cols
34
+ @rows = rows
35
+ @label = label
36
+ @refresh_every = refresh_every
37
+ @description = description
38
+ end
39
+
40
+ def label
41
+ return @label.to_s if @label.present?
42
+ return self.class.label.to_s if self.class.label.present?
43
+
44
+ self.class.id.to_s.humanize
45
+ end
46
+
47
+ def description
48
+ @description || self.class.description
49
+ end
50
+
51
+ def refresh_every
52
+ @refresh_every || self.class.refresh_every
53
+ end
54
+
55
+ def translated_range(range)
56
+ return "#{range} days" if range.is_a? Integer
57
+
58
+ case range
59
+ when "MTD"
60
+ "Month to date"
61
+ when "QTD"
62
+ "Quarter to date"
63
+ when "YTD"
64
+ "Year to date"
65
+ when "TODAY"
66
+ "Today"
67
+ else
68
+ range
69
+ end
70
+ end
71
+
72
+ def parsed_ranges
73
+ return unless ranges.present?
74
+
75
+ ranges.map { |range| [translated_range(range), range] }
76
+ end
77
+
78
+ def turbo_frame
79
+ "#{dashboard.id}_#{id}"
80
+ end
81
+
82
+ def frame_url(enforced_range: nil, params: {})
83
+ enforced_range ||= initial_range || ranges.first
84
+
85
+ # append the parent params to the card request
86
+ begin
87
+ other_params = "&#{params.permit!.to_h.map { |k, v| "#{k}=#{v}" }.join("&")}"
88
+ rescue
89
+ end
90
+
91
+ "#{Avo::App.root_path}/dashboards/#{dashboard.id}/cards/#{id}?turbo_frame=#{turbo_frame}&index=#{index}&range=#{enforced_range}#{other_params}"
92
+ end
93
+
94
+ def card_classes
95
+ result = ""
96
+
97
+ # Writing down the classes so TailwindCSS knows not to purge them
98
+ classes_for_cols = {
99
+ 1 => " sm:col-span-1",
100
+ 2 => " sm:col-span-2",
101
+ 3 => " sm:col-span-3",
102
+ 4 => " sm:col-span-4",
103
+ 5 => " sm:col-span-5",
104
+ 6 => " sm:col-span-6"
105
+ }
106
+
107
+ classes_for_rows = {
108
+ 1 => " h-36",
109
+ 2 => " h-72",
110
+ 3 => " h-[27rem]",
111
+ 4 => " h-[36rem]",
112
+ 5 => " h-[45rem]",
113
+ 6 => " h-[54rem]"
114
+ }
115
+ # puts ["cols->", cols, classes_for_cols, classes_for_rows, classes_for_cols[cols.to_i]].inspect
116
+
117
+ result += classes_for_cols[cols.to_i] if classes_for_cols[cols.to_i].present?
118
+ result += classes_for_rows[rows.to_i] if classes_for_rows[rows.to_i].present?
119
+
120
+ result
121
+ end
122
+
123
+ def type
124
+ return :metric if self.class.superclass == ::Avo::Dashboards::MetricCard
125
+ return :chartkick if self.class.superclass == ::Avo::Dashboards::ChartkickCard
126
+ return :partial if self.class.superclass == ::Avo::Dashboards::PartialCard
127
+ end
128
+
129
+ def compute_result
130
+ Avo::Hosts::DashboardCard.new(card: self, dashboard: dashboard, params: params, context: context, range: range, options: options)
131
+ .compute_result
132
+
133
+ self
134
+ end
135
+
136
+ def hydrate(dashboard: nil, params: nil)
137
+ @dashboard = dashboard if dashboard.present?
138
+ @params = params if params.present?
139
+
140
+ self
141
+ end
142
+
143
+ def range
144
+ return params[:range] if params.dig(:range).present?
145
+
146
+ return initial_range if initial_range.present?
147
+
148
+ ranges.first
149
+ end
150
+
151
+ def result(data)
152
+ self.result_data = data
153
+
154
+ self
155
+ end
156
+
157
+ def is_card?
158
+ true
159
+ end
160
+
161
+ def is_divider?
162
+ false
163
+ end
164
+
165
+ private
166
+
167
+ def cols
168
+ @cols || self.class.cols
169
+ end
170
+
171
+ def rows
172
+ @rows || self.class.rows
173
+ end
174
+ end
175
+ end
@@ -39,7 +39,10 @@ module Avo
39
39
  class_attribute :resolve_query_scope
40
40
  class_attribute :resolve_find_scope
41
41
  class_attribute :ordering
42
- class_attribute :search_hide_from_global_search, default: false
42
+ class_attribute :hide_from_global_search, default: false
43
+ class_attribute :after_create_path, default: :show
44
+ class_attribute :after_update_path, default: :show
45
+ class_attribute :invalid_fields
43
46
 
44
47
  class << self
45
48
  delegate :t, to: ::I18n
@@ -282,6 +285,9 @@ module Avo
282
285
  end
283
286
 
284
287
  def name
288
+ # return 'hwhwhw'
289
+
290
+
285
291
  default = class_name_without_resource.titlecase
286
292
 
287
293
  return @name if @name.present?
@@ -427,11 +433,15 @@ module Avo
427
433
  # This is used as the model class ID
428
434
  # We use this instead of the route_key to maintain compatibility with uncountable models
429
435
  # With uncountable models route key appends an _index suffix (Fish->fish_index)
430
- # Example: User->users, MediaItem->medie_items, Fish->fish
436
+ # Example: User->users, MediaItem->media_items, Fish->fish
431
437
  def model_key
432
438
  model_class.model_name.plural
433
439
  end
434
440
 
441
+ def model_name
442
+ model_class.model_name
443
+ end
444
+
435
445
  def singular_model_key
436
446
  model_class.model_name.singular
437
447
  end
@@ -9,12 +9,30 @@ module Avo
9
9
  class_attribute :items_holder
10
10
  class_attribute :grid_cols, default: 3
11
11
  class_attribute :visible, default: true
12
+ class_attribute :index, default: 0
12
13
 
13
14
  class << self
14
- def card(klass)
15
+ def card(klass, label: nil, description: nil, cols: nil, rows: nil, refresh_every: nil, options: {})
15
16
  self.items_holder ||= []
16
17
 
17
- self.items_holder << klass.new(dashboard: self)
18
+ self.items_holder << klass.new(dashboard: self,
19
+ label: label,
20
+ description: description,
21
+ cols: cols,
22
+ rows: rows,
23
+ refresh_every: refresh_every,
24
+ options: options,
25
+ index: index
26
+ )
27
+ self.index += 1
28
+ end
29
+
30
+ def item_at_index(index)
31
+ items.find do |item|
32
+ next if item.index.blank?
33
+
34
+ item.index == index
35
+ end
18
36
  end
19
37
 
20
38
  def divider(**args)
@@ -3,12 +3,14 @@ module Avo
3
3
  class BaseDivider
4
4
  attr_reader :label
5
5
  attr_reader :invisible
6
+ attr_reader :index
6
7
 
7
8
  class_attribute :id
8
9
 
9
- def initialize(label: nil, invisible: false)
10
+ def initialize(label: nil, invisible: false, index: nil)
10
11
  @label = label
11
12
  @invisible = invisible
13
+ @index = index
12
14
  end
13
15
 
14
16
  def is_divider?
@@ -0,0 +1,6 @@
1
+ module Avo
2
+ module Dashboards
3
+ class DashboardCard < Avo::BaseCard
4
+ end
5
+ end
6
+ end
@@ -1,6 +1,6 @@
1
1
  module Avo
2
2
  module Dashboards
3
- class PartialCard < BaseCard
3
+ class PartialCard < Avo::BaseCard
4
4
  class_attribute :partial
5
5
  end
6
6
  end
@@ -1,7 +1,7 @@
1
1
  module Avo
2
2
  module DynamicRouter
3
3
  def self.routes(router)
4
- Rails.application.eager_load!
4
+ Rails.application.eager_load! if Rails.env.development?
5
5
 
6
6
  BaseResource.descendants
7
7
  .select do |resource|