avo 1.4.2 → 1.5.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 (63) hide show
  1. checksums.yaml +4 -4
  2. data/Gemfile +3 -0
  3. data/Gemfile.lock +63 -54
  4. data/README.md +5 -4
  5. data/app/components/avo/views/resource_index_component.html.erb +3 -3
  6. data/app/controllers/avo/application_controller.rb +14 -7
  7. data/app/controllers/avo/relations_controller.rb +1 -1
  8. data/app/controllers/avo/search_controller.rb +50 -35
  9. data/app/helpers/avo/application_helper.rb +4 -6
  10. data/app/packs/entrypoints/application.css +2 -1
  11. data/app/packs/entrypoints/application.js +3 -0
  12. data/app/packs/js/controllers/search_controller.js +120 -0
  13. data/app/packs/js/helpers/debounce_promise.js +13 -0
  14. data/app/packs/stylesheets/search.css +79 -0
  15. data/app/views/avo/base/index.html.erb +2 -2
  16. data/app/views/avo/base/show.html.erb +2 -2
  17. data/app/views/avo/partials/_global_search.html.erb +17 -0
  18. data/app/views/avo/partials/_profile_dropdown.html.erb +12 -8
  19. data/app/views/avo/partials/_resource_search.html.erb +10 -0
  20. data/app/views/avo/partials/_turbo_frame_wrap.html.erb +3 -0
  21. data/app/views/layouts/avo/application.html.erb +2 -4
  22. data/avo.gemspec +1 -0
  23. data/config/routes.rb +2 -6
  24. data/lib/avo/app.rb +3 -1
  25. data/lib/avo/base_resource.rb +83 -7
  26. data/lib/avo/configuration.rb +2 -0
  27. data/lib/avo/fields/base_field.rb +6 -0
  28. data/lib/avo/fields/external_image_field.rb +1 -0
  29. data/lib/avo/fields/file_field.rb +4 -0
  30. data/lib/avo/licensing/license.rb +10 -0
  31. data/lib/avo/licensing/license_manager.rb +0 -2
  32. data/lib/avo/licensing/pro_license.rb +3 -1
  33. data/lib/avo/services/authorization_service.rb +1 -1
  34. data/lib/avo/version.rb +1 -1
  35. data/lib/generators/avo/templates/initializer/avo.tt +1 -0
  36. data/lib/generators/avo/templates/resource/resource.tt +3 -0
  37. data/lib/generators/avo/templates/tool/sidebar_item.tt +1 -1
  38. data/public/avo-packs/css/{application-68691c73.css → application-d69fe930.css} +183 -6
  39. data/public/avo-packs/css/application-d69fe930.css.br +0 -0
  40. data/public/avo-packs/css/application-d69fe930.css.gz +0 -0
  41. data/public/avo-packs/css/application-d69fe930.css.map +1 -0
  42. data/public/avo-packs/css/application-d69fe930.css.map.br +0 -0
  43. data/public/avo-packs/css/application-d69fe930.css.map.gz +0 -0
  44. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js +26 -0
  45. data/public/avo-packs/js/{application-6a0b9e58526ae6bef242.js.LICENSE.txt → application-7b3d507875f4bc1f6677.js.LICENSE.txt} +0 -0
  46. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.br +0 -0
  47. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.gz +0 -0
  48. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map +1 -0
  49. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map.br +0 -0
  50. data/public/avo-packs/js/application-7b3d507875f4bc1f6677.js.map.gz +0 -0
  51. data/public/avo-packs/manifest.json +15 -15
  52. metadata +35 -15
  53. data/public/avo-packs/css/application-68691c73.css.br +0 -0
  54. data/public/avo-packs/css/application-68691c73.css.gz +0 -0
  55. data/public/avo-packs/css/application-68691c73.css.map +0 -1
  56. data/public/avo-packs/css/application-68691c73.css.map.br +0 -0
  57. data/public/avo-packs/css/application-68691c73.css.map.gz +0 -0
  58. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js +0 -26
  59. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.br +0 -0
  60. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.gz +0 -0
  61. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map +0 -1
  62. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map.br +0 -0
  63. data/public/avo-packs/js/application-6a0b9e58526ae6bef242.js.map.gz +0 -0
@@ -31,12 +31,10 @@ module Avo
31
31
  render partial: "avo/partials/empty_state", locals: {resource_name: resource_name}
32
32
  end
33
33
 
34
- def turbo_frame_start(name)
35
- "<turbo-frame id=\"#{name}\">".html_safe if name.present?
36
- end
37
-
38
- def turbo_frame_end(name)
39
- "</turbo-frame>".html_safe if name.present?
34
+ def turbo_frame_wrap(name, &block)
35
+ render layout: "avo/partials/turbo_frame_wrap", locals: {name: name} do
36
+ capture(&block)
37
+ end
40
38
  end
41
39
 
42
40
  def a_button(label = nil, **args, &block)
@@ -4,7 +4,7 @@
4
4
  @import './../../../node_modules/tippy.js/themes/light.css';
5
5
  @import './../../../node_modules/trix/dist/trix.css';
6
6
  @import './../../../node_modules/flatpickr/dist/flatpickr.css';
7
-
7
+ @import './../../../node_modules/@algolia/autocomplete-theme-classic/dist/theme.css';
8
8
 
9
9
  @import './../stylesheets/tailwindcss/base.css';
10
10
 
@@ -15,6 +15,7 @@
15
15
  @import './../stylesheets/loader.css';
16
16
  @import './../stylesheets/pagination.css';
17
17
  @import './../stylesheets/breadcrumbs.css';
18
+ @import './../stylesheets/search.css';
18
19
 
19
20
  @import './../stylesheets/components/status.css';
20
21
  @import './../stylesheets/components/code.css';
@@ -45,6 +45,9 @@ document.addEventListener('turbo:load', () => {
45
45
  })
46
46
  document.addEventListener('turbo:visit', () => document.body.classList.add('turbo-loading'))
47
47
  document.addEventListener('turbo:submit-start', () => document.body.classList.add('turbo-loading'))
48
+ document.addEventListener('turbo:before-cache', () => {
49
+ document.querySelectorAll('[data-turbo-remove-before-cache]').forEach((element) => element.remove())
50
+ })
48
51
 
49
52
  // Uncomment to copy all static images under ../images to the output folder and reference
50
53
  // them with the image_pack_tag helper in views (e.g <%= image_pack_tag 'rails.png' %>)
@@ -0,0 +1,120 @@
1
+ /* eslint-disable no-underscore-dangle */
2
+ import * as Mousetrap from 'mousetrap'
3
+ import { Controller } from 'stimulus'
4
+ import { Turbo } from '@hotwired/turbo-rails'
5
+ import { autocomplete } from '@algolia/autocomplete-js'
6
+ import debouncePromise from '@/js/helpers/debounce_promise'
7
+
8
+ export default class extends Controller {
9
+ static targets = ['autocomplete', 'button']
10
+
11
+ debouncedFetch = debouncePromise(fetch, this.debounceTimeout)
12
+
13
+ get translationKeys() {
14
+ let keys
15
+ try {
16
+ keys = JSON.parse(this.autocompleteTarget.dataset.translationKeys)
17
+ } catch (error) {
18
+ keys = {}
19
+ }
20
+
21
+ return keys
22
+ }
23
+
24
+ get debounceTimeout() {
25
+ return this.autocompleteTarget.dataset.debounceTimeout
26
+ }
27
+
28
+ get searchResource() {
29
+ return this.autocompleteTarget.dataset.searchResource
30
+ }
31
+
32
+ get isGlobalSearch() {
33
+ return this.searchResource === 'global'
34
+ }
35
+
36
+ get searchUrl() {
37
+ return this.isGlobalSearch ? '/avo/avo_api/search' : `/avo/avo_api/${this.searchResource}/search`
38
+ }
39
+
40
+ addSource(resourceName, data) {
41
+ const that = this
42
+
43
+ return {
44
+ sourceId: resourceName,
45
+ getItems: () => data.results,
46
+ onSelect({ item }) {
47
+ Turbo.visit(item._url, { action: 'replace' })
48
+ },
49
+ templates: {
50
+ header() {
51
+ return data.header
52
+ },
53
+ item({ item, createElement }) {
54
+ let element = ''
55
+
56
+ if (item._avatar) {
57
+ let classes
58
+
59
+ switch (item._avatar_type) {
60
+ default:
61
+ case 'circle':
62
+ classes = 'rounded-full'
63
+ break
64
+ case 'rounded':
65
+ classes = 'rounded'
66
+ break
67
+ }
68
+
69
+ element += `<img src="${item._avatar}" alt="${item._label}" class="flex-shrink-0 w-8 h-8 my-[2px] inline mr-2 ${classes}" />`
70
+ }
71
+ element += `<div>${item._label}`
72
+
73
+ if (item._description) {
74
+ element += `<div class="aa-ItemDescription">${item._description}</div>`
75
+ }
76
+
77
+ element += '</div>'
78
+
79
+ return createElement('div', {
80
+ class: 'flex',
81
+ dangerouslySetInnerHTML: {
82
+ __html: element,
83
+ },
84
+ })
85
+ },
86
+ noResults() {
87
+ return that.translationKeys.no_item_found.replace('%{item}', resourceName)
88
+ },
89
+ },
90
+ }
91
+ }
92
+
93
+ showSearchPanel() {
94
+ this.autocompleteTarget.querySelector('button').click()
95
+ }
96
+
97
+ connect() {
98
+ const that = this
99
+
100
+ this.buttonTarget.onclick = () => this.showSearchPanel()
101
+
102
+ if (this.isGlobalSearch) {
103
+ Mousetrap.bind(['command+k', 'ctrl+k'], () => this.showSearchPanel())
104
+ }
105
+
106
+ autocomplete({
107
+ container: this.autocompleteTarget,
108
+ placeholder: 'Search',
109
+ openOnFocus: true,
110
+ detachedMediaQuery: '',
111
+ getSources: ({ query }) => {
112
+ const endpoint = `${that.searchUrl}?q=${query}`
113
+
114
+ return that.debouncedFetch(endpoint)
115
+ .then((response) => response.json())
116
+ .then((data) => Object.keys(data).map((resourceName) => that.addSource(resourceName, data[resourceName])))
117
+ },
118
+ })
119
+ }
120
+ }
@@ -0,0 +1,13 @@
1
+ export default (fn, time) => {
2
+ let timerId
3
+
4
+ return (...args) => {
5
+ if (timerId) {
6
+ clearTimeout(timerId)
7
+ }
8
+
9
+ return new Promise((resolve) => {
10
+ timerId = setTimeout(() => resolve(fn(...args)), time)
11
+ })
12
+ }
13
+ }
@@ -0,0 +1,79 @@
1
+ :root {
2
+ --aa-primary-color: --tw-ring-color;
3
+ --aa-selected-color: --tw-ring-color;
4
+ --aa-primary-color-rgb: 5, 150, 105;
5
+ }
6
+
7
+ .global-search {
8
+ .aa-DetachedSearchButton:focus,
9
+ .aa-DetachedSearchButton {
10
+ border: none !important;
11
+ }
12
+ }
13
+
14
+ .resource-search {
15
+ .aa-Autocomplete {
16
+ @apply w-full;
17
+ }
18
+
19
+ .aa-DetachedSearchButton {
20
+ @apply rounded-full border-gray-300;
21
+ }
22
+ }
23
+
24
+ .aa-SourceHeader {
25
+ @apply uppercase text-xs font-semibold;
26
+ }
27
+
28
+ .aa-DetachedFormContainer,
29
+ .aa-DetachedContainer .aa-Panel {
30
+ @apply bg-blue-gray-100 border-b-0;
31
+ }
32
+
33
+ .aa-Form {
34
+ &:focus-within {
35
+ @apply ring-0 !important;
36
+ }
37
+
38
+ input {
39
+ @apply focus:ring-0 !important;
40
+ }
41
+ &:focus-within {
42
+ .aa-InputWrapperPrefix{
43
+ .aa-SubmitButton {
44
+ @apply ring-0;
45
+ }
46
+ }
47
+ }
48
+ }
49
+ .aa-Input {
50
+ @apply focus:ring-green-600;
51
+ }
52
+
53
+ .aa-DetachedContainer {
54
+ .aa-PanelLayout {
55
+ @apply pt-0 space-y-3;
56
+ }
57
+
58
+ .aa-Source {
59
+ .aa-List {
60
+ @apply space-y-1;
61
+
62
+ .aa-Item {
63
+ @apply bg-white rounded-md px-3 py-4 shadow;
64
+
65
+ .aa-ItemDescription {
66
+ @apply text-gray-500 text-sm;
67
+ }
68
+
69
+ &[aria-selected=true]{
70
+ @apply bg-blue-700 text-white;
71
+
72
+ .aa-ItemDescription {
73
+ @apply text-white;
74
+ }
75
+ }
76
+ }
77
+ }
78
+ }
79
+ }
@@ -1,4 +1,4 @@
1
- <%= turbo_frame_start params[:turbo_frame] %>
1
+ <%= turbo_frame_wrap(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceIndexComponent.new(
3
3
  resource: @resource,
4
4
  resources: @resources,
@@ -12,4 +12,4 @@
12
12
  parent_model: @parent_model,
13
13
  )
14
14
  %>
15
- <%= turbo_frame_end params[:turbo_frame] %>
15
+ <% end %>
@@ -1,3 +1,3 @@
1
- <%= turbo_frame_start params[:turbo_frame] %>
1
+ <%= turbo_frame_wrap(params[:turbo_frame]) do %>
2
2
  <%= render Avo::Views::ResourceShowComponent.new(resource: @resource, reflection: @reflection) %>
3
- <%= turbo_frame_end params[:turbo_frame] %>
3
+ <% end %>
@@ -0,0 +1,17 @@
1
+ <div data-controller="search" class="global-search" data-turbo-remove-before-cache>
2
+ <div class="inline-block"
3
+ data-search-target="autocomplete"
4
+ data-search-resource="global"
5
+ data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>"}'
6
+ data-debounce-timeout='<%= Avo.configuration.search_debounce %>'
7
+ >
8
+ </div>
9
+ <div class="relative top-[-5px] inline-flex text-gray-400 text-sm leading-5 py-0.5 px-1.5 border border-gray-300 rounded-md cursor-pointer"
10
+ data-search-target="button"
11
+ >
12
+ <span class="sr-only">Press </span>
13
+ <kbd class="font-sans"><abbr title="Command" class="no-underline">⌘</abbr></kbd>
14
+ <span class="sr-only"> and </span>
15
+ <kbd class="font-sans">K</kbd><span class="sr-only"> to search</span>
16
+ </div>
17
+ </div>
@@ -1,19 +1,23 @@
1
1
  <div data-controller="toggle-panel">
2
2
  <a href="javascript:void(0);" class="flex items-center cursor-pointer font-semibold text-gray-700" data-action="click->toggle-panel#togglePanel">
3
- <% if _current_user.respond_to? 'avatar' %>
4
- <%= image_tag _current_user.avatar, class: "h-12 rounded-full border-4 border-white mr-1" if _current_user.avatar.present? %>
3
+ <% if _current_user.respond_to?(:avatar) && _current_user.avatar.present? %>
4
+ <%= image_tag _current_user.avatar, class: "h-12 rounded-full border-4 border-white mr-1" %>
5
5
  <% end %>
6
- <% if _current_user.name.present? %>
6
+ <% if _current_user.respond_to?(:name) && _current_user.name.present? %>
7
7
  <%= _current_user.name %>
8
- <% elsif _current_user.email.present? %>
8
+ <% elsif _current_user.respond_to?(:email) && _current_user.email.present? %>
9
9
  <%= _current_user.email %>
10
10
  <% else %>
11
11
  Avo user
12
12
  <% end %>
13
- <%= svg 'chevron-down', class: "ml-1 h-4" %>
13
+ <% if main_app.respond_to?(:destroy_user_session_path) %>
14
+ <%= svg 'chevron-down', class: "ml-1 h-4" %>
15
+ <% end %>
14
16
  </a>
15
17
 
16
- <div class="hidden absolute inset-auto inset-auto right-0 mr-6 mt-0 py-4 bg-white rounded-xl min-w-[200px] shadow-context" data-toggle-panel-target="panel">
17
- <%= button_to "Sign out", main_app.destroy_user_session_path, method: :delete, form: { "data-turbo" => "false" }, class: "appearance-none bg-white text-left cursor-pointer text-green-600 font-semibold hover:text-white hover:bg-green-500 block px-4 py-1 w-full" %>
18
- </div>
18
+ <% if main_app.respond_to?(:destroy_user_session_path) %>
19
+ <div class="hidden absolute inset-auto inset-auto right-0 mr-6 mt-0 py-4 bg-white rounded-xl min-w-[200px] shadow-context" data-toggle-panel-target="panel">
20
+ <%= button_to "Sign out", main_app.destroy_user_session_path, method: :delete, form: { "data-turbo" => "false" }, class: "appearance-none bg-white text-left cursor-pointer text-green-600 font-semibold hover:text-white hover:bg-green-500 block px-4 py-1 w-full" %>
21
+ </div>
22
+ <% end %>
19
23
  </div>
@@ -0,0 +1,10 @@
1
+ <div data-controller="search" class="resource-search flex items-center h-full w-full" data-turbo-remove-before-cache>
2
+ <div class="w-full"
3
+ data-search-target="autocomplete"
4
+ data-search-resource="<%= resource %>"
5
+ data-translation-keys='{"no_item_found": "<%= I18n.translate 'avo.no_item_found' %>"}'
6
+ data-debounce-timeout='<%= Avo.configuration.search_debounce %>'
7
+ >
8
+ </div>
9
+ <div class="relative inline-flex text-gray-400 text-sm border border-gray-300 rounded-full cursor-pointer" data-search-target="button"></div>
10
+ </div>
@@ -0,0 +1,3 @@
1
+ <% if name.present? %><turbo-frame id="<%= name %>"><% end %>
2
+ <%= yield %>
3
+ <% if name.present? %></turbo-frame><% end %>
@@ -23,10 +23,8 @@
23
23
  <div>
24
24
  <%= render partial: "avo/partials/header" %>
25
25
  </div>
26
- <div class="flex-1 flex justify-center">
27
- <div class="w-64 flex">
28
- <%# <resources-search :global="true"> %>
29
- </div>
26
+ <div class="flex-1 flex ml-4 pl-4">
27
+ <%= render partial: "avo/partials/global_search" if ::Avo::App.license.has_with_trial(:global_search) %>
30
28
  </div>
31
29
  <div class="align-end">
32
30
  <%= render partial: "avo/partials/profile_dropdown" %>
data/avo.gemspec CHANGED
@@ -45,4 +45,5 @@ Gem::Specification.new do |spec|
45
45
  spec.add_dependency "meta-tags"
46
46
  spec.add_dependency "breadcrumbs_on_rails"
47
47
  spec.add_dependency "manifester"
48
+ spec.add_dependency "ransack"
48
49
  end
data/config/routes.rb CHANGED
@@ -4,6 +4,8 @@ Avo::Engine.routes.draw do
4
4
  get "resources", to: redirect("/avo")
5
5
 
6
6
  scope "avo_api", as: "avo_api" do
7
+ get "/search", to: "search#index"
8
+ get "/:resource_name/search", to: "search#show"
7
9
  post "/resources/:resource_name/:id/attachments/", to: "attachments#create"
8
10
  end
9
11
 
@@ -27,10 +29,4 @@ Avo::Engine.routes.draw do
27
29
  post "/:resource_name/:id/:related_name", to: "relations#create"
28
30
  delete "/:resource_name/:id/:related_name/:related_id", to: "relations#destroy"
29
31
  end
30
-
31
- # get '/avo-api/search', to: 'search#index'
32
- # get '/avo-api/:resource_name/search', to: 'search#resource'
33
-
34
- # Tools
35
- # get '/avo-tools/resource-overview', to: 'resource_overview#index'
36
32
  end
data/lib/avo/app.rb CHANGED
@@ -6,6 +6,7 @@ module Avo
6
6
  class_attribute :request, default: nil
7
7
  class_attribute :context, default: nil
8
8
  class_attribute :license, default: nil
9
+ class_attribute :current_user, default: nil
9
10
 
10
11
  class << self
11
12
  def boot
@@ -20,9 +21,10 @@ module Avo
20
21
  end
21
22
  end
22
23
 
23
- def init(request:, context:)
24
+ def init(request:, context:, current_user:)
24
25
  self.request = request
25
26
  self.context = context
27
+ self.current_user = current_user
26
28
 
27
29
  self.license = Licensing::LicenseManager.new(Licensing::HQ.new(request).response).license
28
30
 
@@ -11,7 +11,7 @@ module Avo
11
11
 
12
12
  class_attribute :id, default: :id
13
13
  class_attribute :title, default: :id
14
- class_attribute :search, default: [:id]
14
+ class_attribute :search_query, default: nil
15
15
  class_attribute :includes, default: []
16
16
  class_attribute :model_class
17
17
  class_attribute :translation_key
@@ -42,6 +42,18 @@ module Avo
42
42
 
43
43
  self.filters_loader.use filter_class
44
44
  end
45
+
46
+ def scope
47
+ authorization.apply_policy model_class
48
+ end
49
+
50
+ def authorization
51
+ Avo::Services::AuthorizationService.new Avo::App.current_user
52
+ end
53
+ end
54
+
55
+ def initialize
56
+ self.class.model_class = model_class
45
57
  end
46
58
 
47
59
  def hydrate(model: nil, view: nil, user: nil, params: nil)
@@ -65,7 +77,7 @@ module Avo
65
77
  field.hydrate(resource: self, panel_name: default_panel_name, user: user)
66
78
  end
67
79
 
68
- if Avo::App.license.invalid? || Avo::App.license.lacks(:custom_fields)
80
+ if Avo::App.license.has_with_trial(:custom_fields)
69
81
  fields = fields.reject do |field|
70
82
  field.custom?
71
83
  end
@@ -96,9 +108,9 @@ module Avo
96
108
  end
97
109
  end
98
110
 
99
- fields.map do |field|
100
- field.hydrate(model: @model, view: @view, resource: self)
101
- end
111
+ hydrate_fields(model: @model, view: @view)
112
+
113
+ fields
102
114
  end
103
115
 
104
116
  def get_grid_fields
@@ -119,8 +131,16 @@ module Avo
119
131
  self.class.actions_loader.bag
120
132
  end
121
133
 
134
+ def hydrate_fields(model: nil, view: nil)
135
+ fields.map do |field|
136
+ field.hydrate(model: @model, view: @view, resource: self)
137
+ end
138
+
139
+ self
140
+ end
141
+
122
142
  def default_panel_name
123
- return @params[:related_name].capitalize if @params[:related_name].present?
143
+ return @params[:related_name].capitalize if @params.present? && @params[:related_name].present?
124
144
 
125
145
  case @view
126
146
  when :show
@@ -226,7 +246,11 @@ module Avo
226
246
 
227
247
  def fill_model(model, params)
228
248
  # Map the received params to their actual fields
229
- fields_by_database_id = get_field_definitions.map { |field| [field.database_id(model).to_s, field] }.to_h
249
+ fields_by_database_id = get_field_definitions
250
+ .reject do |field|
251
+ field.computed
252
+ end
253
+ .map { |field| [field.database_id(model).to_s, field] }.to_h
230
254
 
231
255
  params.each do |key, value|
232
256
  field = fields_by_database_id[key]
@@ -301,5 +325,57 @@ module Avo
301
325
  end
302
326
  end
303
327
  end
328
+
329
+ def avo_path
330
+ "#{Avo.configuration.root_path}/resources/#{model_class.model_name.route_key}/#{model.id}"
331
+ end
332
+
333
+ def label_field
334
+ get_field_definitions.find do |field|
335
+ field.as_label.present?
336
+ end
337
+ rescue
338
+ nil
339
+ end
340
+
341
+ def label
342
+ label_field.value || model_title
343
+ rescue
344
+ model_title
345
+ end
346
+
347
+ def avatar_field
348
+ get_field_definitions.find do |field|
349
+ field.as_avatar.present?
350
+ end
351
+ rescue
352
+ nil
353
+ end
354
+
355
+ def avatar
356
+ avatar_field.to_image
357
+ rescue
358
+ nil
359
+ end
360
+
361
+ def avatar_type
362
+ avatar_field.as_avatar
363
+ rescue
364
+ nil
365
+ end
366
+
367
+ def description_field
368
+ get_field_definitions.find do |field|
369
+ field.as_description.present?
370
+ end
371
+ rescue
372
+ nil
373
+ end
374
+
375
+ def description
376
+ description_field.value
377
+ rescue
378
+ nil
379
+ end
304
380
  end
305
381
  end