avo 2.1.2.pre2 → 2.2.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 (85) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +4 -2
  3. data/Gemfile.lock +11 -5
  4. data/app/assets/svgs/{dashboards-icon.svg → dashboards.svg} +1 -1
  5. data/app/assets/svgs/{resources-icon.svg → resources.svg} +0 -0
  6. data/app/assets/svgs/{tools-icon.svg → tools.svg} +0 -0
  7. data/app/components/avo/button_component.rb +6 -2
  8. data/app/components/avo/fields/belongs_to_field/autocomplete_component.html.erb +1 -0
  9. data/app/components/avo/fields/belongs_to_field/edit_component.rb +5 -2
  10. data/app/components/avo/index/field_wrapper_component.rb +13 -0
  11. data/app/components/avo/index/grid_item_component.html.erb +3 -1
  12. data/app/components/avo/index/table_row_component.html.erb +7 -5
  13. data/app/components/avo/panel_component.html.erb +4 -4
  14. data/app/components/avo/profile_item_component.html.erb +2 -2
  15. data/app/components/avo/profile_item_component.rb +6 -0
  16. data/app/components/avo/sidebar/base_item_component.rb +31 -0
  17. data/app/components/avo/sidebar/group_component.html.erb +28 -0
  18. data/app/components/avo/sidebar/group_component.rb +4 -0
  19. data/app/components/avo/sidebar/heading_component.html.erb +14 -0
  20. data/app/components/avo/sidebar/heading_component.rb +15 -0
  21. data/app/components/avo/sidebar/item_switcher_component.html.erb +16 -0
  22. data/app/components/avo/sidebar/item_switcher_component.rb +15 -0
  23. data/app/components/avo/sidebar/link_component.html.erb +12 -0
  24. data/app/components/avo/sidebar/link_component.rb +28 -0
  25. data/app/components/avo/sidebar/section_component.html.erb +15 -0
  26. data/app/components/avo/sidebar/section_component.rb +9 -0
  27. data/app/components/avo/sidebar_component.html.erb +33 -24
  28. data/app/components/avo/sidebar_component.rb +3 -9
  29. data/app/components/avo/sidebar_profile_component.html.erb +10 -1
  30. data/app/components/avo/views/resource_edit_component.html.erb +2 -2
  31. data/app/components/avo/views/resource_new_component.html.erb +2 -2
  32. data/app/components/avo/views/resource_show_component.html.erb +2 -2
  33. data/app/controllers/avo/application_controller.rb +1 -2
  34. data/app/controllers/avo/base_controller.rb +1 -1
  35. data/app/helpers/avo/application_helper.rb +2 -0
  36. data/app/javascript/js/controllers/loading_button_controller.js +47 -10
  37. data/app/javascript/js/controllers/menu_controller.js +60 -0
  38. data/app/javascript/js/controllers/search_controller.js +28 -10
  39. data/app/javascript/js/controllers.js +2 -0
  40. data/app/views/avo/base/_boolean_filter.html.erb +1 -1
  41. data/app/views/avo/base/_multiple_select_filter.html.erb +7 -4
  42. data/app/views/avo/base/_select_filter.html.erb +1 -1
  43. data/app/views/avo/base/_text_filter.html.erb +1 -1
  44. data/app/views/avo/partials/_table_header.html.erb +12 -13
  45. data/app/views/layouts/avo/application.html.erb +3 -0
  46. data/avo.gemspec +1 -0
  47. data/bin/helpers.rb +7 -1
  48. data/bin/init +2 -2
  49. data/lib/avo/app.rb +8 -86
  50. data/lib/avo/base_resource.rb +1 -3
  51. data/lib/avo/concerns/fetches_things.rb +127 -0
  52. data/lib/avo/configuration.rb +4 -0
  53. data/lib/avo/dynamic_router.rb +1 -1
  54. data/lib/avo/engine.rb +0 -1
  55. data/lib/avo/fields/base_field.rb +2 -0
  56. data/lib/avo/fields/belongs_to_field.rb +2 -0
  57. data/lib/avo/hosts/base_host.rb +20 -0
  58. data/lib/avo/licensing/pro_license.rb +2 -1
  59. data/lib/avo/menu/base_item.rb +20 -0
  60. data/lib/avo/menu/builder.rb +77 -0
  61. data/lib/avo/menu/dashboard.rb +17 -0
  62. data/lib/avo/menu/group.rb +2 -0
  63. data/lib/avo/menu/link.rb +4 -0
  64. data/lib/avo/menu/menu.rb +2 -0
  65. data/lib/avo/menu/resource.rb +9 -0
  66. data/lib/avo/menu/section.rb +2 -0
  67. data/lib/avo/reloader.rb +15 -7
  68. data/lib/avo/version.rb +1 -1
  69. data/lib/generators/avo/filter_generator.rb +2 -0
  70. data/lib/generators/avo/templates/filters/boolean_filter.tt +1 -1
  71. data/lib/generators/avo/templates/filters/multiple_select_filter.tt +11 -0
  72. data/lib/generators/avo/templates/filters/select_filter.tt +1 -1
  73. data/lib/generators/avo/templates/filters/text_filter.tt +1 -1
  74. data/lib/generators/avo/templates/tool/sidebar_item.tt +1 -1
  75. data/public/avo-assets/avo.css +23 -6
  76. data/public/avo-assets/avo.js +63 -63
  77. data/public/avo-assets/avo.js.map +3 -3
  78. metadata +44 -14
  79. data/app/assets/builds/avo.css +0 -8810
  80. data/app/assets/builds/avo.js +0 -423
  81. data/app/assets/builds/avo.js.map +0 -7
  82. data/app/components/avo/sidebar_heading_component.html.erb +0 -3
  83. data/app/components/avo/sidebar_heading_component.rb +0 -11
  84. data/app/components/avo/sidebar_item_component.html.erb +0 -3
  85. data/app/components/avo/sidebar_item_component.rb +0 -10
@@ -35,12 +35,12 @@
35
35
  method: :delete,
36
36
  local: true,
37
37
  title: t('avo.delete_item', item: @resource.model.model_name.name.downcase).capitalize,
38
- spinner: true,
38
+ loading: true,
39
+ confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
39
40
  color: :red,
40
41
  icon: 'trash',
41
42
  form_class: 'flex flex-col sm:flex-row sm:inline-flex',
42
43
  data: {
43
- confirm: t('avo.are_you_sure', item: @resource.model.model_name.name.downcase),
44
44
  control: :destroy,
45
45
  'resource-id': @resource.model.id,
46
46
  'tippy': 'tooltip',
@@ -19,7 +19,6 @@ module Avo
19
19
  before_action :set_container_classes
20
20
  before_action :add_initial_breadcrumbs
21
21
  before_action :set_view
22
- before_action :set_model_to_fill
23
22
 
24
23
  rescue_from Pundit::NotAuthorizedError, with: :render_unauthorized
25
24
  rescue_from ActiveRecord::RecordInvalid, with: :exception_logger
@@ -285,7 +284,7 @@ module Avo
285
284
  end
286
285
 
287
286
  def set_locale
288
- I18n.locale = params[:locale] || I18n.default_locale
287
+ I18n.locale = params[:set_locale] || I18n.default_locale
289
288
 
290
289
  I18n.default_locale = I18n.locale
291
290
  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(Arel.sql("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}"))
47
+ @query = @query.order("#{@resource.model_class.table_name}.#{@index_params[:sort_by]} #{@index_params[:sort_direction]}")
48
48
  end
49
49
 
50
50
  # Apply filters
@@ -71,6 +71,8 @@ module Avo
71
71
  end
72
72
 
73
73
  def svg(file_name, **args)
74
+ return if file_name.nil?
75
+
74
76
  options = {}
75
77
  options[:class] = args[:class].present? ? args[:class] : ""
76
78
  options[:class] += args[:extra_class].present? ? " #{args[:extra_class]}" : ""
@@ -1,3 +1,4 @@
1
+ /* eslint-disable no-alert */
1
2
  import { Controller } from '@hotwired/stimulus'
2
3
 
3
4
  export default class extends Controller {
@@ -6,17 +7,53 @@ export default class extends Controller {
6
7
  <div class="double-bounce2"></div>
7
8
  </div>`;
8
9
 
10
+ confirmed = false
11
+
9
12
  connect() {
10
- const button = this.context.scope.element
11
- this.context.scope.element.addEventListener('click', () => {
12
- button.style.width = `${button.getBoundingClientRect().width}px`
13
- button.style.height = `${button.getBoundingClientRect().height}px`
14
- button.innerHTML = this.spinnerMarkup
15
- button.classList.add('justify-center')
16
-
17
- setTimeout(() => {
18
- button.setAttribute('disabled', 'disabled')
19
- }, 1)
13
+ this.context.scope.element.addEventListener('click', (e) => {
14
+ // If the user has to confirm the action
15
+ if (this.confirmationMessage) {
16
+ // Intervene only if not confirmed
17
+ if (!this.confirmed) {
18
+ e.preventDefault()
19
+ if (window.confirm(this.confirmationMessage)) {
20
+ this.applyLoader()
21
+ }
22
+ }
23
+ } else {
24
+ this.applyLoader()
25
+ }
20
26
  })
21
27
  }
28
+
29
+ get button() {
30
+ return this.context.scope.element
31
+ }
32
+
33
+ get confirmationMessage() {
34
+ return this.context.scope.element.getAttribute('data-avo-confirm')
35
+ }
36
+
37
+ applyLoader() {
38
+ const { button } = this
39
+
40
+ button.style.width = `${button.getBoundingClientRect().width}px`
41
+ button.style.height = `${button.getBoundingClientRect().height}px`
42
+ button.innerHTML = this.spinnerMarkup
43
+ button.classList.add('justify-center')
44
+
45
+ setTimeout(() => {
46
+ this.markConfirmed()
47
+ button.click()
48
+ button.setAttribute('disabled', 'disabled')
49
+ }, 1)
50
+ }
51
+
52
+ markConfirmed() {
53
+ this.confirmed = true
54
+ }
55
+
56
+ markUnconfirmed() {
57
+ this.confirmed = false
58
+ }
22
59
  }
@@ -0,0 +1,60 @@
1
+ import { Controller } from '@hotwired/stimulus'
2
+ import isNull from 'lodash/isNull'
3
+
4
+ export default class extends Controller {
5
+ static targets = ['svg', 'items', 'self'];
6
+
7
+ collapsed = false;
8
+
9
+ get key() {
10
+ return this.selfTarget.getAttribute('data-menu-key-param')
11
+ }
12
+
13
+ defaultState() {
14
+ return this.selfTarget.getAttribute('data-menu-collapsed-param') === 'collapsed'
15
+ }
16
+
17
+ connect() {
18
+ if (this.getState() === 'collapsed') {
19
+ this.collapsed = true
20
+ this.markCollapsed()
21
+ } else if (isNull(this.getState()) && this.defaultState()) {
22
+ this.collapsed = true
23
+ this.markCollapsed()
24
+ }
25
+ }
26
+
27
+ triggerCollapse() {
28
+ this.collapsed = !this.collapsed
29
+
30
+ this.updateDom()
31
+ }
32
+
33
+ updateDom() {
34
+ if (this.collapsed) {
35
+ this.markCollapsed()
36
+ } else {
37
+ this.markExpanded()
38
+ }
39
+ }
40
+
41
+ markCollapsed() {
42
+ this.svgTarget.classList.add('rotate-90')
43
+ this.itemsTarget.classList.add('hidden')
44
+ this.storeState('collapsed')
45
+ }
46
+
47
+ markExpanded() {
48
+ this.svgTarget.classList.remove('rotate-90')
49
+ this.itemsTarget.classList.remove('hidden')
50
+ this.storeState('expanded')
51
+ }
52
+
53
+ getState() {
54
+ return window.localStorage.getItem(this.key)
55
+ }
56
+
57
+ storeState(payload) {
58
+ window.localStorage.setItem(this.key, payload)
59
+ }
60
+ }
@@ -88,6 +88,9 @@ export default class extends Controller {
88
88
  } else {
89
89
  Turbo.visit(item._url, { action: 'advance' })
90
90
  }
91
+
92
+ // On searchable belongs to the class `aa-Detached` remains on the body making it unscrollable
93
+ document.body.classList.remove('aa-Detached')
91
94
  }
92
95
 
93
96
  addSource(resourceName, data) {
@@ -102,7 +105,7 @@ export default class extends Controller {
102
105
  return `${data.header.toUpperCase()} ${data.help}`
103
106
  },
104
107
  item({ item, createElement }) {
105
- let element = ''
108
+ const children = []
106
109
 
107
110
  if (item._avatar) {
108
111
  let classes
@@ -117,22 +120,37 @@ export default class extends Controller {
117
120
  break
118
121
  }
119
122
 
120
- element += `<img src="${item._avatar}" alt="${item._label}" class="flex-shrink-0 w-8 h-8 my-[2px] inline mr-2 ${classes}" />`
123
+ children.push(
124
+ createElement('img', {
125
+ src: item._avatar,
126
+ alt: item._label,
127
+ class: `flex-shrink-0 w-8 h-8 my-[2px] inline mr-2 ${classes}`,
128
+ }),
129
+ )
121
130
  }
122
- element += `<div>${item._label}`
123
131
 
132
+ const labelChildren = [item._label]
124
133
  if (item._description) {
125
- element += `<div class="aa-ItemDescription">${item._description}</div>`
134
+ labelChildren.push(
135
+ createElement(
136
+ 'div',
137
+ {
138
+ class: 'aa-ItemDescription',
139
+ },
140
+ item._description,
141
+ ),
142
+ )
126
143
  }
127
144
 
128
- element += '</div>'
145
+ children.push(createElement('div', null, labelChildren))
129
146
 
130
- return createElement('div', {
131
- class: 'flex',
132
- dangerouslySetInnerHTML: {
133
- __html: element,
147
+ return createElement(
148
+ 'div',
149
+ {
150
+ class: 'flex',
134
151
  },
135
- })
152
+ children,
153
+ )
136
154
  },
137
155
  noResults() {
138
156
  return that.translationKeys.no_item_found.replace(
@@ -15,6 +15,7 @@ import ItemSelectAllController from './controllers/item_select_all_controller'
15
15
  import ItemSelectorController from './controllers/item_selector_controller'
16
16
  import KeyValueController from './controllers/fields/key_value_controller'
17
17
  import LoadingButtonController from './controllers/loading_button_controller'
18
+ import MenuController from './controllers/menu_controller'
18
19
  import MobileController from './controllers/mobile_controller'
19
20
  import ModalController from './controllers/modal_controller'
20
21
  import MultipleSelectFilterController from './controllers/multiple_select_filter_controller'
@@ -39,6 +40,7 @@ application.register('hidden-input', HiddenInputController)
39
40
  application.register('item-select-all', ItemSelectAllController)
40
41
  application.register('item-selector', ItemSelectorController)
41
42
  application.register('loading-button', LoadingButtonController)
43
+ application.register('menu', MenuController)
42
44
  application.register('mobile', MobileController)
43
45
  application.register('modal', ModalController)
44
46
  application.register('multiple-select-filter', MultipleSelectFilterController)
@@ -11,7 +11,7 @@
11
11
  end
12
12
  set_value = {} if set_value.nil?
13
13
  %>
14
- <div data-controller="boolean-filter">
14
+ <div data-controller="boolean-filter" data-filter-name="<%= filter.name %>">
15
15
  <%= filter_wrapper name: filter.name do %>
16
16
  <div class="flex items-center">
17
17
  <div class="space-y-2">
@@ -1,12 +1,15 @@
1
1
  <%
2
+ set_value = filter.default.present? ? filter.default.select { |key, value| value }.keys.map(&:to_sym) : {}
3
+
2
4
  begin
3
5
  decoded_filters_param = JSON.parse(Base64.decode64(params[:filters]))
4
- set_value = decoded_filters_param[filter.class.to_s]
5
- rescue => exception
6
- set_value = filter.options.keys
6
+ if decoded_filters_param[filter.class.to_s].present?
7
+ set_value = decoded_filters_param[filter.class.to_s]
8
+ end
9
+ rescue
7
10
  end
8
11
  %>
9
- <div data-controller="multiple-select-filter">
12
+ <div data-controller="multiple-select-filter" data-filter-name="<%= filter.name %>">
10
13
  <%= filter_wrapper name: filter.name do %>
11
14
  <%= select_tag filter.id, options_for_select(filter.options.invert, set_value),
12
15
  class: input_classes('w-full mb-0'),
@@ -6,7 +6,7 @@
6
6
  set_value = filter.default
7
7
  end
8
8
  %>
9
- <div data-controller="select-filter">
9
+ <div data-controller="select-filter" data-filter-name="<%= filter.name %>">
10
10
  <%= filter_wrapper name: filter.name do %>
11
11
  <%= select_tag filter.id, options_for_select(filter.options.invert, set_value),
12
12
  class: input_classes('w-full mb-0'),
@@ -6,7 +6,7 @@
6
6
  set_value = filter.default
7
7
  end
8
8
  %>
9
- <div data-controller="text-filter">
9
+ <div data-controller="text-filter" data-filter-name="<%= filter.name %>">
10
10
  <%= filter_wrapper name: filter.name do %>
11
11
  <%= text_field_tag filter.id, set_value,
12
12
  class: input_classes('w-full mb-0'),
@@ -1,10 +1,11 @@
1
-
2
1
  <thead class="bg-white border-b border-gray-200 pb-1">
3
- <th class="rounded-lg">
4
- <%== item_select_all_input %>
5
- </th>
6
- <%
7
- fields.each_with_index do |field, index|
2
+ <% if @resource.record_selector %>
3
+ <th class="rounded-lg">
4
+ <%== item_select_all_input %>
5
+ </th>
6
+ <% end %>
7
+ <% fields.each_with_index do |field, index| %>
8
+ <%
8
9
  if params[:sort_by] == field.id.to_s
9
10
  if params[:sort_direction] == 'asc'
10
11
  sort_by = nil
@@ -28,8 +29,7 @@
28
29
  sort_by = field.id
29
30
  sort_direction = 'desc'
30
31
  end
31
- classes = "text-gray-500 tracking-tight leading-tight text-sm font-semibold"
32
- %>
32
+ classes = "text-gray-500 tracking-tight leading-tight text-sm font-semibold" %>
33
33
  <th class="text-left uppercase px-3 py-4 whitespace-nowrap rounded-l">
34
34
  <% if field.sortable %>
35
35
  <%= link_to params.permit!.merge(sort_by: sort_by, sort_direction: sort_direction), class: "flex items-center #{classes} #{'cursor-pointer' if field.sortable}", 'data-turbo-frame': params[:turbo_frame] do %>
@@ -37,14 +37,13 @@
37
37
  <%= render partial: 'avo/partials/sortable_component', locals: {field: field} %>
38
38
  <% end %>
39
39
  <% else %>
40
- <div class="flex items-center <%= classes %>">
41
- <%= field.name %>
42
- </div>
40
+ <div class="flex items-center <%= classes %>">
41
+ <%= field.name %>
42
+ </div>
43
43
  <% end %>
44
44
  </th>
45
45
  <% end %>
46
-
47
46
  <th class="w-24">
48
- <!-- Controls cell -->
47
+ <!-- Item controls cell -->
49
48
  </th>
50
49
  </thead>
@@ -16,6 +16,9 @@
16
16
  <% else %>
17
17
  <%= javascript_include_tag "avo", "data-turbo-track": "reload", defer: true %>
18
18
  <%= stylesheet_link_tag "avo", "data-turbo-track": "reload", defer: true %>
19
+ <% if Rails.env.development? %>
20
+ <%= javascript_include_tag "hotwire-livereload", defer: true %>
21
+ <% end %>
19
22
  <% end %>
20
23
  </head>
21
24
  <body class="bg-application os-mac">
data/avo.gemspec CHANGED
@@ -47,4 +47,5 @@ Gem::Specification.new do |spec|
47
47
  spec.add_dependency "breadcrumbs_on_rails"
48
48
  spec.add_dependency "chartkick"
49
49
  spec.add_dependency "dry-initializer"
50
+ spec.add_dependency "docile"
50
51
  end
data/bin/helpers.rb CHANGED
@@ -22,8 +22,14 @@ end
22
22
 
23
23
  def ask(question:, valid_answers: [])
24
24
  puts "\n#{question} (#{valid_answers.join('/')})"
25
+ # An uppercase option is treated as a default answer. Otherwise, we disregard case, and always
26
+ # return the answer in lowercase.
27
+ default_answer = valid_answers.select { |val| val == val.upcase }.first&.downcase
25
28
 
26
- input = gets.chomp
29
+ valid_answers.map!(&:downcase)
30
+
31
+ input = gets.downcase.chomp
32
+ input = default_answer if input == ''
27
33
 
28
34
  while !valid_answers.include?(input)
29
35
  puts 'Invalid input, please try again.'
data/bin/init CHANGED
@@ -17,7 +17,7 @@ app_root do
17
17
  header 'Installing Yarn packages'
18
18
  run! 'yarn'
19
19
 
20
- if use_docker
20
+ if use_docker == 'y'
21
21
  header 'Creating the Docker volume'
22
22
  run! 'docker volume create --name=avo-db-data'
23
23
 
@@ -28,7 +28,7 @@ app_root do
28
28
  header 'Preparing the database'
29
29
  run! 'bin/rails db:setup'
30
30
 
31
- if use_docker
31
+ if use_docker == 'y'
32
32
  header 'Stopping the Docker image'
33
33
  run! 'docker-compose stop'
34
34
  end
data/lib/avo/app.rb CHANGED
@@ -1,5 +1,7 @@
1
1
  module Avo
2
2
  class App
3
+ include Avo::Concerns::FetchesThings
4
+
3
5
  class_attribute :resources, default: []
4
6
  class_attribute :dashboards, default: []
5
7
  class_attribute :cache_store, default: nil
@@ -102,96 +104,16 @@ module Avo
102
104
  end
103
105
  end
104
106
 
105
- # Returns the Avo dashboard by id
106
- def get_dashboard_by_id(id)
107
- dashboards.find do |dashboard|
108
- dashboard.id == id
109
- end
110
- end
111
-
112
- # Returns the Avo resource by camelized name
113
- #
114
- # get_resource_by_name('User') => UserResource
115
- def get_resource(resource)
116
- resources.find do |available_resource|
117
- "#{resource}Resource".safe_constantize == available_resource.class
118
- end
119
- end
120
-
121
- # Returns the Avo resource by singular snake_cased name
122
- #
123
- # get_resource_by_name('user') => UserResource
124
- def get_resource_by_name(name)
125
- get_resource name.singularize.camelize
126
- end
127
-
128
- # Returns the Avo resource by singular snake_cased name
129
- #
130
- # get_resource_by_name('User') => UserResource
131
- # get_resource_by_name(User) => UserResource
132
- def get_resource_by_model_name(name)
133
- resources.find do |resource|
134
- resource.model_class.model_name.name == name.to_s
135
- end
136
- end
137
-
138
- # Returns the Avo resource by singular snake_cased name
139
- #
140
- # get_resource_by_controller_name('delayed_backend_active_record_jobs') => DelayedJobResource
141
- # get_resource_by_controller_name('users') => UserResource
142
- def get_resource_by_controller_name(name)
143
- resources.find do |resource|
144
- resource.model_class.to_s.pluralize.underscore.tr("/", "_") == name.to_s
145
- end
146
- end
147
-
148
- # Returns the Rails model class by singular snake_cased name
149
- #
150
- # get_model_class_by_name('user') => User
151
- def get_model_class_by_name(name)
152
- name.to_s.camelize.singularize
153
- end
107
+ def main_menu
108
+ return nil if Avo::App.license.lacks_with_trial(:menu_builder)
154
109
 
155
- def get_available_resources(user = nil)
156
- resources.select do |resource|
157
- Services::AuthorizationService.authorize user, resource.model_class, Avo.configuration.authorization_methods.stringify_keys["index"], raise_exception: false
158
- end
159
- .sort_by { |r| r.name }
110
+ Avo::Menu::Builder.parse_menu(&Avo.configuration.main_menu)
160
111
  end
161
112
 
162
- def get_available_dashboards(user = nil)
163
- dashboards.sort_by { |r| r.name }
164
- end
113
+ def profile_menu
114
+ return nil if Avo::App.license.lacks_with_trial(:menu_builder)
165
115
 
166
- def resources_navigation(user = nil)
167
- get_available_resources(user)
168
- .select do |resource|
169
- resource.model_class.present?
170
- end
171
- .select do |resource|
172
- resource.visible_on_sidebar
173
- end
174
- end
175
-
176
- def get_dashboards(user = nil)
177
- return [] unless App.license.has_with_trial(:resource_ordering)
178
-
179
- get_available_dashboards(user)
180
- end
181
-
182
- # Insert any partials that we find in app/views/avo/sidebar/items.
183
- def get_sidebar_partials
184
- Dir.glob(Rails.root.join("app", "views", "avo", "sidebar", "items", "*.html.erb"))
185
- .map do |path|
186
- File.basename path
187
- end
188
- .map do |filename|
189
- # remove the leading underscore (_)
190
- filename[0] = ""
191
- # remove the extension
192
- filename.gsub!(".html.erb", "")
193
- filename
194
- end
116
+ Avo::Menu::Builder.parse_menu(&Avo.configuration.profile_menu)
195
117
  end
196
118
  end
197
119
  end
@@ -43,6 +43,7 @@ module Avo
43
43
  class_attribute :after_create_path, default: :show
44
44
  class_attribute :after_update_path, default: :show
45
45
  class_attribute :invalid_fields
46
+ class_attribute :record_selector, default: true
46
47
 
47
48
  class << self
48
49
  delegate :t, to: ::I18n
@@ -285,9 +286,6 @@ module Avo
285
286
  end
286
287
 
287
288
  def name
288
- # return 'hwhwhw'
289
-
290
-
291
289
  default = class_name_without_resource.titlecase
292
290
 
293
291
  return @name if @name.present?
@@ -0,0 +1,127 @@
1
+ module Avo
2
+ module Concerns
3
+ module FetchesThings
4
+ extend ActiveSupport::Concern
5
+
6
+ class_methods do
7
+ # Returns the Avo dashboard by id
8
+ #
9
+ # get_dashboard_by_id(:dashy) -> Dashy
10
+ def get_dashboard_by_id(id)
11
+ dashboards.find do |dashboard|
12
+ dashboard.id == id
13
+ end
14
+ end
15
+
16
+ # Returns the Avo dashboard by name
17
+ #
18
+ # get_dashboard_by_name(:dashy) -> Dashy
19
+ def get_dashboard_by_name(name)
20
+ dashboards.find do |dashboard|
21
+ dashboard.name == name
22
+ end
23
+ end
24
+
25
+ # Returns the Avo resource by camelized name
26
+ #
27
+ # get_resource_by_name('User') => UserResource
28
+ def get_resource(resource)
29
+ possible_resource = "#{resource}Resource".gsub "ResourceResource", "Resource"
30
+
31
+ resources.find do |available_resource|
32
+ possible_resource.safe_constantize == available_resource.class
33
+ end
34
+ end
35
+
36
+ # Returns the Avo resource by singular snake_cased name
37
+ #
38
+ # get_resource_by_name('user') => UserResource
39
+ def get_resource_by_name(name)
40
+ get_resource name.singularize.camelize
41
+ end
42
+
43
+ # Returns the Avo resource by singular snake_cased name
44
+ #
45
+ # get_resource_by_name('User') => UserResource
46
+ # get_resource_by_name(User) => UserResource
47
+ def get_resource_by_model_name(name)
48
+ resources.find do |resource|
49
+ resource.model_class.model_name.name == name.to_s
50
+ end
51
+ end
52
+
53
+ # Returns the Avo resource by singular snake_cased name
54
+ #
55
+ # get_resource_by_controller_name('delayed_backend_active_record_jobs') => DelayedJobResource
56
+ # get_resource_by_controller_name('users') => UserResource
57
+ def get_resource_by_controller_name(name)
58
+ resources.find do |resource|
59
+ resource.model_class.to_s.pluralize.underscore.tr("/", "_") == name.to_s
60
+ end
61
+ end
62
+
63
+ # Returns the Avo resource by some name
64
+ def guess_resource(name)
65
+ get_resource_by_name(name.to_s) || get_resource_by_model_name(name)
66
+ end
67
+
68
+ # Returns the Rails model class by singular snake_cased name
69
+ #
70
+ # get_model_class_by_name('user') => User
71
+ def get_model_class_by_name(name)
72
+ name.to_s.camelize.singularize
73
+ end
74
+
75
+ def get_available_resources(user = nil)
76
+ resources.select do |resource|
77
+ Services::AuthorizationService.authorize user, resource.model_class, Avo.configuration.authorization_methods.stringify_keys["index"], raise_exception: false
78
+ end
79
+ .sort_by { |r| r.name }
80
+ end
81
+
82
+ def get_available_dashboards(user = nil)
83
+ dashboards.sort_by { |r| r.name }
84
+ end
85
+
86
+ def resources_for_navigation(user = nil)
87
+ get_available_resources(current_user)
88
+ .select do |resource|
89
+ resource.model_class.present?
90
+ end
91
+ .select do |resource|
92
+ resource.visible_on_sidebar
93
+ end
94
+ end
95
+
96
+ def dashboards_for_navigation(user = nil)
97
+ return [] if App.license.lacks_with_trial(:resource_ordering)
98
+
99
+ get_available_dashboards(user).select do |dashboard|
100
+ dashboard.is_visible?
101
+ end
102
+ end
103
+
104
+ # Insert any partials that we find in app/views/avo/sidebar/items.
105
+ def get_sidebar_partials
106
+ Dir.glob(Rails.root.join("app", "views", "avo", "sidebar", "items", "*.html.erb"))
107
+ .map do |path|
108
+ File.basename path
109
+ end
110
+ .map do |filename|
111
+ # remove the leading underscore (_)
112
+ filename[0] = ""
113
+ # remove the extension
114
+ filename.gsub!(".html.erb", "")
115
+ filename
116
+ end
117
+ end
118
+
119
+ def tools_for_navigation
120
+ return [] if Avo::App.license.lacks_with_trial(:custom_tools)
121
+
122
+ get_sidebar_partials
123
+ end
124
+ end
125
+ end
126
+ end
127
+ end