avo 2.0.0 → 2.1.2.pre1

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 (74) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +1 -1
  3. data/Gemfile.lock +3 -3
  4. data/app/assets/builds/avo.css +8810 -0
  5. data/app/assets/builds/avo.js +423 -0
  6. data/app/assets/builds/avo.js.map +7 -0
  7. data/app/assets/stylesheets/avo.css +1 -1
  8. data/app/assets/svgs/save.svg +8 -1
  9. data/app/components/avo/actions_component.html.erb +1 -1
  10. data/app/components/avo/alert_component.html.erb +1 -1
  11. data/app/components/avo/alert_component.rb +6 -6
  12. data/app/components/avo/blank_field_component.html.erb +0 -0
  13. data/app/components/avo/blank_field_component.rb +4 -0
  14. data/app/components/avo/card_component.html.erb +7 -2
  15. data/app/components/avo/fields/badge_field/index_component.html.erb +1 -1
  16. data/app/components/avo/fields/boolean_field/index_component.html.erb +1 -1
  17. data/app/components/avo/fields/external_image_field/index_component.html.erb +1 -2
  18. data/app/components/avo/fields/file_field/index_component.html.erb +3 -3
  19. data/app/components/avo/fields/file_field/index_component.rb +11 -0
  20. data/app/components/avo/fields/gravatar_field/index_component.html.erb +1 -1
  21. data/app/components/avo/fields/progress_bar_field/index_component.html.erb +1 -1
  22. data/app/components/avo/index/field_wrapper_component.html.erb +1 -1
  23. data/app/components/avo/index/field_wrapper_component.rb +12 -1
  24. data/app/components/avo/index/resource_controls_component.html.erb +5 -1
  25. data/app/components/avo/index/resource_table_component.html.erb +1 -1
  26. data/app/components/avo/panel_component.html.erb +15 -5
  27. data/app/components/avo/panel_component.rb +9 -0
  28. data/app/components/avo/referrer_params_component.html.erb +4 -0
  29. data/app/components/avo/referrer_params_component.rb +9 -0
  30. data/app/components/avo/sidebar_component.html.erb +2 -0
  31. data/app/components/avo/sidebar_component.rb +3 -1
  32. data/app/components/avo/views/resource_edit_component.html.erb +14 -1
  33. data/app/components/avo/views/resource_new_component.html.erb +13 -0
  34. data/app/components/avo/views/resource_show_component.html.erb +11 -0
  35. data/app/components/avo/views/resource_show_component.rb +5 -0
  36. data/app/controllers/avo/application_controller.rb +1 -1
  37. data/app/controllers/avo/base_controller.rb +87 -37
  38. data/app/controllers/avo/dashboards_controller.rb +2 -6
  39. data/app/controllers/avo/search_controller.rb +5 -1
  40. data/app/helpers/avo/url_helpers.rb +6 -2
  41. data/app/javascript/avo.js +4 -3
  42. data/app/views/avo/actions/show.html.erb +1 -0
  43. data/app/views/avo/associations/new.html.erb +1 -0
  44. data/app/views/avo/dashboards/show.html.erb +4 -1
  45. data/app/views/avo/home/failed_to_load.html.erb +21 -1
  46. data/app/views/avo/partials/_sidebar_extra.html.erb +0 -0
  47. data/app/views/layouts/avo/application.html.erb +7 -0
  48. data/bin/dev +7 -6
  49. data/lib/avo/app.rb +8 -1
  50. data/lib/avo/base_action.rb +5 -4
  51. data/lib/avo/base_card.rb +175 -0
  52. data/lib/avo/base_resource.rb +34 -8
  53. data/lib/avo/configuration.rb +2 -0
  54. data/lib/avo/dashboards/base_dashboard.rb +37 -2
  55. data/lib/avo/dashboards/base_divider.rb +3 -1
  56. data/lib/avo/dashboards/chartkick_card.rb +1 -1
  57. data/lib/avo/dashboards/dashboard_card.rb +6 -0
  58. data/lib/avo/dashboards/partial_card.rb +1 -1
  59. data/lib/avo/fields/base_field.rb +19 -6
  60. data/lib/avo/fields/belongs_to_field.rb +1 -1
  61. data/lib/avo/fields/date_field.rb +1 -1
  62. data/lib/avo/fields/external_image_field.rb +2 -2
  63. data/lib/avo/fields_collector.rb +7 -2
  64. data/lib/avo/hosts/dashboard_card.rb +1 -0
  65. data/lib/avo/hosts/dashboard_visibility.rb +19 -0
  66. data/lib/avo/licensing/pro_license.rb +1 -0
  67. data/lib/avo/version.rb +1 -1
  68. data/lib/generators/avo/templates/dashboards/dashboard.tt +4 -1
  69. data/public/avo-assets/avo.css +55 -34
  70. data/public/avo-assets/avo.js +166 -165
  71. data/public/avo-assets/avo.js.map +3 -3
  72. metadata +15 -6
  73. data/app/views/avo/partials/_failed_state.html.erb +0 -16
  74. data/lib/avo/dashboards/base_card.rb +0 -151
@@ -10,7 +10,11 @@ module Avo
10
10
  def index
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
- render json: search_resources(Avo::App.resources)
13
+ resources = Avo::App.resources.reject do |resource|
14
+ resource.class.hide_from_global_search
15
+ end
16
+
17
+ render json: search_resources(resources)
14
18
  end
15
19
 
16
20
  def show
@@ -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(
@@ -71,12 +71,13 @@ document.addEventListener('turbo:frame-load', () => {
71
71
  initTippy()
72
72
  })
73
73
 
74
- document.addEventListener('turbo:before-fetch-response', (e) => {
74
+ document.addEventListener('turbo:before-fetch-response', async (e) => {
75
75
  if (e.detail.fetchResponse.response.status === 500) {
76
- const id = e.srcElement.getAttribute('id')
77
- e.srcElement.src = `${window.Avo.configuration.root_path}/failed_to_load?turbo_frame=${id}`
76
+ const { id, src } = e.target
77
+ e.target.src = `${window.Avo.configuration.root_path}/failed_to_load?turbo_frame=${id}&src=${src}`
78
78
  }
79
79
  })
80
+
80
81
  document.addEventListener('turbo:visit', () => document.body.classList.add('turbo-loading'))
81
82
  document.addEventListener('turbo:submit-start', () => document.body.classList.add('turbo-loading'))
82
83
  document.addEventListener('turbo:submit-end', () => document.body.classList.remove('turbo-loading'))
@@ -10,6 +10,7 @@
10
10
  <%= form_with model: @model,
11
11
  scope: 'fields',
12
12
  url: "#{@resource.records_path}/actions/#{@action.param_id}",
13
+ local: true,
13
14
  data: @action.class.form_data_attributes do |form|
14
15
  %>
15
16
  <%= render Avo::ModalComponent.new do |c| %>
@@ -1,6 +1,7 @@
1
1
  <%= turbo_frame_tag 'attach_modal' do %>
2
2
  <%= form_with scope: 'fields',
3
3
  url: "#{avo.root_path}resources/#{params[:resource_name]}/#{params[:id]}/#{params[:related_name]}/",
4
+ local: true,
4
5
  data: {
5
6
  'turbo-frame': '_top'
6
7
  } do |form| %>
@@ -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 %>
@@ -1,3 +1,23 @@
1
1
  <%= turbo_frame_wrap(params[:turbo_frame]) do %>
2
- <%= render 'avo/partials/failed_state' %>
2
+ <%
3
+ classes = 'absolute inset-auto left-1/2 top-1/2 transform -translate-x-1/2 -translate-y-1/2'
4
+ label = t 'avo.failed_to_load'
5
+ %>
6
+ <div class="relative flex-1 py-12">
7
+ <div class="relative block text-gray-300 h-40 w-full">
8
+ <%= svg 'avocado', class: "#{classes} h-20 text-gray-400" %>
9
+ <%= svg 'code', class: "#{classes} h-8 -ml-20 -mt-12" %>
10
+ <%= svg 'fire', class: "#{classes} h-8 -ml-10 -mt-24" %>
11
+ <%= svg 'color-swatch', class: "#{classes} h-8 ml-8 -mt-24" %>
12
+ <%= svg 'globe', class: "#{classes} h-8 ml-20 -mt-12" %>
13
+ <%= svg 'library', class: "#{classes} h-8 -ml-20 mt-4" %>
14
+ <%= svg 'photograph', class: "#{classes} h-8 ml-20 mt-4" %>
15
+ </div>
16
+ <div class="relative block text-center text-lg text-gray-400 font-semibold -mt-10"><%= label %> <span class="border-b-2 border-dashed"><%= params[:turbo_frame].to_s.humanize.downcase if params[:turbo_frame].present? %></span> frame</div>
17
+ </div>
18
+ <% if Rails.env.development? %>
19
+ <div class="text-center text-sm w-full">
20
+ This is not an issue with Avo. Click <%= link_to 'this link', params[:src], target: :_blank %> to see why this frame failed to load.
21
+ </div>
22
+ <% end %>
3
23
  <% end %>
File without changes
@@ -42,6 +42,13 @@
42
42
 
43
43
  <%= turbo_frame_tag 'alerts', class: "fixed inset-0 bottom-0 flex flex-col space-y-4 items-end justify-right px-4 py-6 sm:p-6 justify-end z-50 pointer-events-none" do %>
44
44
  <%= render Avo::FlashAlertsComponent.new flashes: flash %>
45
+
46
+ <% # In case we have other general error messages %>
47
+ <% if @errors.present? %>
48
+ <% @errors.each do |message| %>
49
+ <%= render Avo::AlertComponent.new :error, message %>
50
+ <% end %>
51
+ <% end %>
45
52
  <% end %>
46
53
 
47
54
  <%= render partial: "avo/partials/scripts" %>
data/bin/dev CHANGED
@@ -1,9 +1,10 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- if ! command -v foreman &> /dev/null
4
- then
5
- echo "Installing foreman..."
6
- gem install foreman
7
- fi
3
+ PORT="${PORT:-3030}"
4
+ export PORT
8
5
 
9
- foreman start -f Procfile.dev
6
+ if command -v overmind &> /dev/null; then
7
+ overmind start -f Procfile.dev "$@"
8
+ else
9
+ foreman start -f Procfile.dev "$@"
10
+ fi
data/lib/avo/app.rb CHANGED
@@ -10,6 +10,8 @@ module Avo
10
10
  class_attribute :current_user, default: nil
11
11
  class_attribute :root_path, default: nil
12
12
  class_attribute :view_context, default: nil
13
+ class_attribute :params, default: {}
14
+ class_attribute :translation_enabled, default: false
13
15
 
14
16
  class << self
15
17
  def boot
@@ -24,14 +26,16 @@ module Avo
24
26
  end
25
27
  end
26
28
 
27
- def init(request:, context:, current_user:, root_path:, view_context:)
29
+ def init(request:, context:, current_user:, root_path:, view_context:, params:)
28
30
  self.request = request
29
31
  self.context = context
30
32
  self.current_user = current_user
31
33
  self.root_path = root_path
32
34
  self.view_context = view_context
35
+ self.params = params
33
36
 
34
37
  self.license = Licensing::LicenseManager.new(Licensing::HQ.new(request).response).license
38
+ self.translation_enabled = license.has(:localization)
35
39
 
36
40
  # Set the current host for ActiveStorage
37
41
  begin
@@ -89,6 +93,9 @@ module Avo
89
93
  .select do |dashboard|
90
94
  dashboard != Dashboards::BaseDashboard
91
95
  end
96
+ .uniq do |dashboard|
97
+ dashboard.id
98
+ end
92
99
  end
93
100
 
94
101
  # Returns the Avo dashboard by id
@@ -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
@@ -27,16 +28,16 @@ module Avo
27
28
  def form_data_attributes
28
29
  # We can't respond with a file download from Turbo se we disable it on the form
29
30
  if may_download_file
30
- { 'turbo': false }
31
+ {turbo: false, remote: false}
31
32
  else
32
- { 'turbo-frame': '_top', 'action-target': 'form' }
33
+ {"turbo-frame": "_top", "action-target": "form"}
33
34
  end
34
35
  end
35
36
 
36
37
  # We can't respond with a file download from Turbo se we disable close the modal manually after a while (it's a hack, we know)
37
38
  def submit_button_data_attributes
38
39
  if may_download_file
39
- { action: 'click->modal#delayedClose' }
40
+ {action: "click->modal#delayedClose"}
40
41
  else
41
42
  {}
42
43
  end
@@ -109,7 +110,7 @@ module Avo
109
110
  args = {
110
111
  fields: processed_fields,
111
112
  current_user: current_user,
112
- resource: resource,
113
+ resource: resource
113
114
  }
114
115
 
115
116
  args[:models] = models unless standalone
@@ -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
@@ -11,6 +11,7 @@ module Avo
11
11
  delegate :avo, to: :view_context
12
12
  delegate :resource_path, to: :view_context
13
13
  delegate :resources_path, to: :view_context
14
+ delegate :t, to: ::I18n
14
15
 
15
16
  attr_accessor :view
16
17
  attr_accessor :model
@@ -26,7 +27,7 @@ module Avo
26
27
  class_attribute :includes, default: []
27
28
  class_attribute :model_class
28
29
  class_attribute :translation_key
29
- class_attribute :translation_enabled
30
+ class_attribute :translation_enabled, default: false
30
31
  class_attribute :default_view_type, default: :table
31
32
  class_attribute :devise_password_optional, default: false
32
33
  class_attribute :actions_loader
@@ -38,8 +39,14 @@ module Avo
38
39
  class_attribute :resolve_query_scope
39
40
  class_attribute :resolve_find_scope
40
41
  class_attribute :ordering
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
41
46
 
42
47
  class << self
48
+ delegate :t, to: ::I18n
49
+
43
50
  def grid(&block)
44
51
  grid_collector = GridCollector.new
45
52
  grid_collector.instance_eval(&block)
@@ -96,6 +103,8 @@ module Avo
96
103
  self.class.model_class = model_class.base_class
97
104
  end
98
105
  end
106
+
107
+ self.class.translation_enabled = ::Avo::App.translation_enabled
99
108
  end
100
109
 
101
110
  def hydrate(model: nil, view: nil, user: nil, params: nil)
@@ -215,7 +224,7 @@ module Avo
215
224
  when :edit
216
225
  model_title
217
226
  when :new
218
- I18n.t("avo.create_new_item", item: name.downcase).upcase_first
227
+ t("avo.create_new_item", item: name.downcase).upcase_first
219
228
  end
220
229
  end
221
230
 
@@ -244,9 +253,18 @@ module Avo
244
253
  class_name_without_resource.safe_constantize
245
254
  end
246
255
 
256
+ def model_id
257
+ @model.send id
258
+ end
259
+
247
260
  def model_title
248
- return @model.send title if @model.present?
261
+ return name if @model.nil?
262
+
263
+ the_title = @model.send title
264
+ return the_title if the_title.present?
249
265
 
266
+ model_id
267
+ rescue
250
268
  name
251
269
  end
252
270
 
@@ -267,11 +285,13 @@ module Avo
267
285
  end
268
286
 
269
287
  def name
288
+ default = class_name_without_resource.titlecase
289
+
270
290
  return @name if @name.present?
271
291
 
272
- return I18n.t(translation_key, count: 1).capitalize if translation_key
292
+ return t(translation_key, count: 1, default: default).capitalize if translation_key
273
293
 
274
- class_name_without_resource.titlecase
294
+ default
275
295
  end
276
296
 
277
297
  def singular_name
@@ -279,9 +299,11 @@ module Avo
279
299
  end
280
300
 
281
301
  def plural_name
282
- return I18n.t(translation_key, count: 2).capitalize if translation_key
302
+ default = name.pluralize
303
+
304
+ return t(translation_key, count: 2, default: default).capitalize if translation_key
283
305
 
284
- name.pluralize
306
+ default
285
307
  end
286
308
 
287
309
  def underscore_name
@@ -408,11 +430,15 @@ module Avo
408
430
  # This is used as the model class ID
409
431
  # We use this instead of the route_key to maintain compatibility with uncountable models
410
432
  # With uncountable models route key appends an _index suffix (Fish->fish_index)
411
- # Example: User->users, MediaItem->medie_items, Fish->fish
433
+ # Example: User->users, MediaItem->media_items, Fish->fish
412
434
  def model_key
413
435
  model_class.model_name.plural
414
436
  end
415
437
 
438
+ def model_name
439
+ model_class.model_name
440
+ end
441
+
416
442
  def singular_model_key
417
443
  model_class.model_name.singular
418
444
  end
@@ -28,6 +28,7 @@ module Avo
28
28
  attr_accessor :current_user_resource_name
29
29
  attr_accessor :raise_error_on_missing_policy
30
30
  attr_accessor :disabled_features
31
+ attr_accessor :buttons_on_form_footers
31
32
 
32
33
  def initialize
33
34
  @root_path = "/avo"
@@ -68,6 +69,7 @@ module Avo
68
69
  @current_user_resource_name = "user"
69
70
  @raise_error_on_missing_policy = false
70
71
  @disabled_features = []
72
+ @buttons_on_form_footers = false
71
73
  end
72
74
 
73
75
  def locale_tag
@@ -8,12 +8,31 @@ module Avo
8
8
  class_attribute :description
9
9
  class_attribute :items_holder
10
10
  class_attribute :grid_cols, default: 3
11
+ class_attribute :visible, default: true
12
+ class_attribute :index, default: 0
11
13
 
12
14
  class << self
13
- def card(klass)
15
+ def card(klass, label: nil, description: nil, cols: nil, rows: nil, refresh_every: nil, options: {})
14
16
  self.items_holder ||= []
15
17
 
16
- 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
17
36
  end
18
37
 
19
38
  def divider(**args)
@@ -48,6 +67,22 @@ module Avo
48
67
  def navigation_path
49
68
  "#{Avo::App.root_path}/dashboards/#{id}"
50
69
  end
70
+
71
+ def is_visible?
72
+ # Default is true
73
+ return true if visible == true
74
+
75
+ # Hide if false
76
+ return false if visible == false
77
+
78
+ if visible.respond_to? :call
79
+ ::Avo::Hosts::DashboardVisibility.new(block: visible, dashboard: self).handle
80
+ end
81
+ end
82
+
83
+ def is_hidden?
84
+ !is_visible?
85
+ end
51
86
  end
52
87
  end
53
88
  end
@@ -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?
@@ -24,7 +24,7 @@ module Avo
24
24
 
25
25
  def chartkick_options
26
26
  card_height = 144
27
- card_heading = 32
27
+ card_heading = 40
28
28
 
29
29
  default = {
30
30
  # figure our the available height for the chart
@@ -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
@@ -10,6 +10,7 @@ module Avo
10
10
  delegate :view_context, to: "Avo::App"
11
11
  delegate :main_app, to: :view_context
12
12
  delegate :avo, to: :view_context
13
+ delegate :t, to: ::I18n
13
14
 
14
15
  attr_reader :id
15
16
  attr_reader :block
@@ -48,7 +49,7 @@ module Avo
48
49
  @id = id
49
50
  @name = args[:name]
50
51
  @translation_key = args[:translation_key]
51
- @translation_enabled = false
52
+ @translation_enabled = ::Avo::App.translation_enabled
52
53
  @block = block
53
54
  @required = args[:required] || false
54
55
  @readonly = args[:readonly] || false
@@ -94,18 +95,26 @@ module Avo
94
95
  @translation_key
95
96
  end
96
97
 
98
+ # Getting the name of the resource (user/users, post/posts)
99
+ # We'll first check to see if the user passed a name
100
+ # Secondly we'll try to find a translation key
101
+ # We'll fallback to humanizing the id
97
102
  def name
103
+ default = @id.to_s.humanize(keep_id_suffix: true)
104
+
98
105
  return @name if @name.present?
99
106
 
100
- return I18n.t(translation_key, count: 1).capitalize if translation_key
107
+ return t(translation_key, count: 1, default: default).capitalize if translation_key
101
108
 
102
- @id.to_s.humanize(keep_id_suffix: true)
109
+ default
103
110
  end
104
111
 
105
112
  def plural_name
106
- return I18n.t(translation_key, count: 2).capitalize if translation_key
113
+ default = name.pluralize
114
+
115
+ return t(translation_key, count: 2, default: default).capitalize if translation_key
107
116
 
108
- name.pluralize
117
+ default
109
118
  end
110
119
 
111
120
  def placeholder
@@ -181,8 +190,12 @@ module Avo
181
190
  "#{type.camelize}Field"
182
191
  end
183
192
 
193
+ # Try and build the component class or fallback to a blank one
184
194
  def component_for_view(view = :index)
185
- "Avo::Fields::#{view_component_name}::#{view.to_s.camelize}Component".safe_constantize
195
+ component_class = "::Avo::Fields::#{view_component_name}::#{view.to_s.camelize}Component"
196
+ component_class.constantize
197
+ rescue
198
+ ::Avo::BlankFieldComponent
186
199
  end
187
200
 
188
201
  def model_errors